Linked List implementation in Ruby
My solution with a @tail pointer, althought it added complexity it made adding to tail O(1), also having a variable size @size made it easier to counter edge cases.


Having a tail pointer and a size variable makes totally sense.


  Could the Linked List be optimize?


I don't think you can improve performance much here, you already cover most / all of the corner cases. Well done to implement add_at_index with one iteration. 


  Are you able to understand how is design when reading the code?


Here are some suggestions to make the code more readable

Extract helper methods

In your code you do several times if @head == nil to check if the list is empty. What about extracting an empty? method?

def empty?
  head.nil?
end


Same goes for instance with increasing the size.

  def increase_size
    @size += 1
  end


Use getter / setter methods

You already define getter and setter methods in your class but then fail to use them.

attr_accessor :head, :size, :tail


Instead of doing @head you should use head etc. Another problem is that the getter / setter are public, move them to the private section of the class. There is no need that consumer of this class know about these implementation details.

Implement the Enumerable interface

At several places in your code you need to iterate over your list with a while loop. If you implement the Enumerable interface of Ruby, you only need to do this once.

class MyLinkedList
  include Enumerable

  def initialize
    @head = nil
    @size = 0
    @tail = nil
  end

  def each
    current = head
    until current.nil?
      yield current
      current = current.next
    end
  end

  def [](index)
    each_with_index do |item, i|
      return item.val if i == index
    end
  end


https://ruby-doc.org/core-2.7.1/Enumerable.html

Use Ruby method names

You probably use the method stubs provided from LeetCode but I think it's still worth mentioning. Whenever possible you should use similar methods as already in the Ruby standard library. If we look at the Array class, we see for instance these methods.

def [](index) # get(index)
def unshift(value) # add_at_head(val)
def &lt;&lt;(value) # add_at_tail(val)
def append(value) # add_at_tail(val)
def [](index, value) # def add_at_index(index, val)
def shift # delete_at_head
def pop # delete_at_tail


Summary

Here are some of my suggestions applied

class MyLinkedList
  include Enumerable

  def initialize
    @head = nil
    @size = 0
    @tail = nil
  end

  def [](index)
    return if empty?
    return if index &gt; size - 1
    return head.val if index.zero?
    return tail.val if index == size - 1

    each_with_index do |item, i|
      return item.val if i == index
    end
  end

  def unshift(value)
    node = Node.new(value)
    node.next = head

    @tail = node if empty?
    @head = node
    increase_size

    value
  end

  def &lt;&lt;(value)
    return unshift(value) if empty?

    node = Node.new(value)
    tail.next = node
    @tail = node
    increase_size

    value
  end

  def []=(index, value)
    return add_at_head(value) if index == 0
    return add_at_tail(value) if head == tail
    return add_at_tail(value) if size == index
    return if index &gt; size

    each_with_index do |item, i|
      insert_node(item, value) if i == index - 1
    end
  end

  def each
    current = head
    until current.nil?
      yield current
      current = current.next
    end
  end

  private

  attr_accessor :head, :size, :tail

  def insert_node(item, value)
    tmp = item.next
    node = Node.new(value)
    item.next = node
    node.next = tmp
    increase_size
  end

  def empty?
    @head.nil?
  end

  def increase_size
    @size += 1
  end

  class Node
    attr_accessor :val, :next

    def initialize(val)
      @val = val
      @next = nil
    end
  end
end