Logo

dev-resources.site

for different kinds of informations.

Ruby's Array: a Swiss Army Knife?

Published at
12/15/2019
Categories
ruby
oop
metaprogramming
antipatterns
Author
ngarbezza
Author
9 person written this
ngarbezza
open
Ruby's Array: a Swiss Army Knife?

Intro

In one of the lectures about OOP I usually give at university we go through design patterns and antipatterns. When we first studied the Swiss Army Knife antipattern, the students asked me for an example and I didn't hesitate for a second. I immediately thought of the Array class in Ruby!

Problem

On a very questionable design decision, Ruby has an Array class that can be used as any type of collection. Want a fixed-size collection? use Array. Want a queue or a stack? use Array! Want a simple ordered collection? Use Array!

Next there's a list of all the methods instances of Array understand:

> Array.instance_methods(false)
=> [:transpose, :fill, :assoc, :rassoc, :uniq, :uniq!, :compact, :compact!, :to_h, :flatten, :flatten!, :shuffle!, :include?, :permut
ation, :combination, :sample, :repeated_combination, :shuffle, :product, :bsearch, :bsearch_index, :repeated_permutation, :map!, :&,
:*, :+, :-, :sort, :count, :find_index, :select, :reject, :collect, :map, :pack, :first, :any?, :reverse_each, :zip, :take, :take_whi
le, :drop, :drop_while, :cycle, :insert, :|, :index, :rindex, :replace, :clear, :<=>, :<<, :==, :[], :[]=, :reverse, :empty?, :eql?,
:concat, :reverse!, :inspect, :delete, :length, :size, :each, :slice, :slice!, :to_ary, :to_a, :to_s, :dig, :hash, :at, :fetch, :last
, :push, :pop, :shift, :frozen?, :unshift, :each_index, :join, :rotate, :rotate!, :sort_by!, :collect!, :sort!, :select!, :keep_if, :
values_at, :delete_at, :delete_if, :reject!]

And I'm being a nice fella and didn't include the extension methods added in ActiveSupport (remember #forty_two?) ;-)

Array's multipurpose use goes against several object-oriented principles. There are no single responsibilities and there are groups of messages only valid for certain contexts. The class loses its essence just to become an object that tries to make everyone happy. The different collection types are not reified at all. Furthermore, if I want to use a stack in my program, I would expect a Stack class with an essential protocol of push, pop, top, empty? and nothing else. I'm definitely sure I wouldn't want to retrieve the third element of the stack, for instance. Or to add an element to the bottom.

Collection types

The idea of specific collection types is not new: Smalltalk-80 specification includes (as of today) the best, in my opinion, collection hierarchy with clearly defined collection types: Array, Set, Bag, OrderedCollection, SortedCollection and so on. Even though this collection hierarchy has some design problems, it allows the developers to choose a collection type that fits the domain they are working with. You can use those collections in pretty much any Smalltalk dialect, such as Squeak, Cuis or Pharo.

Possible Solutions

Don't use Array for everything... have you heard about the Set class?

Ruby has a Set class! Unfortunately, I rarely see it on Ruby programs, maybe because it's easier to create an Array. Just type require 'set' and you'll be set (pun intended) to go. Now that you know about it, it's time to use it!

Metaprogramming to make Arrays more specific

Let's say I want to enforce a list to be used as a fixed size collection. What I can do, given I don't want to implement a brand new collection class, is to "select" just the methods we want, depending on the type of collection we need.

So I want to change default Arrayโ€™s behavior (and the way I want to use it) from this:

> my_kind_of_array = [1,2,3]
=> [1, 2, 3]
> my_kind_of_array << 4
=> [1, 2, 3, 4]

to this:

> my_true_array = [1,2,3].as_array
=> [1, 2, 3]
> my_true_array << 4   
NoMethodError: undefined method `<<' for [1, 2, 3]:Array

This would indicate that I want to use my list as a fixed size collection (that's the purpose of the as_array message) and therefore, every method to add/remove elements should not be understood on that object. That way I can have (in a dynamic way and still using native Array methods) the exact essential protocol I need.

Hereโ€™s a way to implement this:

class Array
  def as_array
    instance_eval do
      undef :<< # and do the same for other methods that add/remove elements
    end

    self
  end
end

By using instance_eval I'm able to change that particular instance I'm using, and undef makes sure that the method is completely removed, and for the end-user, it seems the message never existed at all.

I can even change the semantics of some of the messages. For instance, << on a queue could mean "enqueue", and on a stack could mean "push". Everything you want could happen inside an instance_eval block: removing methods, adding new ones, and even changing some semantics. Hereโ€™s an example of a change in the semantics of << to have an array working as a set.

class Array
  def as_set
    instance_eval do
      undef :[], :[]=, :at, :first # and more

      def <<(object)
        super(object) unless include?(object)
      end
    end

    self
  end
end

This is just an experiment, the implementation is not complete but you might get an idea of the final goal.

Conclusions

  • We still have some antipatterns living in our daily languages and tools. But sometimes, as in Ruby, we have the power to modify the language and/or extend it to suit our needs. This is an option we don't usually consider (maybe for historical reasons, and most people say it's "bad" without strong arguments) but it's perfectly valid.
  • Metaprogramming is very helpful in this case, not to add new behavior but to "turn" the swiss army knife in a single, one-purpose tool.
  • If collection classes only have essential behavior, developers get more "educated" and they are "forced" to think which kind of collection they want and use it consistently. This would prevent some unexpected hacks to happen.
antipatterns Article's
30 articles in total
Favicon
Backend Red Flags - What NOT to do
Favicon
Microservice Antipatterns: The Shared Client Library
Favicon
Microservices: Avoiding the Pitfalls, Embracing the Potential - A Guide to Anti-Patterns
Favicon
Antipattern: We'll buy it when we come back
Favicon
SQL antipatterns: Diplomatic Immunity
Favicon
The Consumer Conundrum: Navigating Change in Microservices Without Gridlock
Favicon
Top AWS CloudFormation Anti-Patterns & Best Practices
Favicon
Guess-Driven Development
Favicon
AWS Serverless Anti-Patterns: What They Are and How to Avoid Them
Favicon
AWS DR Anti-Patterns: Avoiding Common Mistakes
Favicon
The Strategy of One
Favicon
Microservices anti-patterns
Favicon
Get out early with Perl statement modifiers
Favicon
JavaScript Anti-patterns
Favicon
The two most common DevOps anti-patterns
Favicon
The God ๐Ÿฆธ
Favicon
Design Meeting Patterns and Antipatterns
Favicon
Anti-patterns of automated software testing
Favicon
React Anti Patterns Part 1
Favicon
Death by Interfaces?
Favicon
The Antipattern Antipattern
Favicon
Ruby's Array: a Swiss Army Knife?
Favicon
13 ways the Internet is broken - #9 will shock you!
Favicon
A note from a TDD zealot
Favicon
Applications as Frameworks
Favicon
My first React 'aha' moment. Is this an antipattern?
Favicon
Avoiding the Builder Design Pattern in Kotlin
Favicon
The Fallacy of DRY
Favicon
Avoid anemic domain models by empowering your objects
Favicon
ๅพฎๆœๅŠก็š„ๅๆจกๅผๅ’Œ้™ท้˜ฑ

Featured ones: