Find file History
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
..
Failed to load latest commit information.
.rubocop.yml
README.md
original_solution.rb
sample_spec.rb
spec.rb

README.md

DataModel

В практиката постоянно ни се налага да работим с бази от данни. За улеснение, използваме библиотеки, които абстрахират работата с подобни системи и ни предоставят удобен интерфейс за заявки. Тези библиотеки се наричат ORM-и. Най-известният ORM за Ruby e ActiveRecord.

В тази задача ще си направим нещо наподобяващо ActiveRecord, но квадрилиони пъти по-просто.

Модели

В нашата импровизирана база от данни можем да съхраняваме различни видове обекти. Класът на всеки от тези обекти се нарича модел. Всеки модел може да има много инстанции (записи в базата ни от данни).

Ето пример за модел:

class User < DataModel
  attributes :name, :email
  data_store ArrayStore.new
end
  • attributes задава имената на атрибутите (колоните, полетата), които ще има всеки запис от типа User.
  • data_store задава хранилището, в което ще се съхраняват записите (вижте секцията за Store-ове).

Всеки клас, наследяващ от DataModel трябва да съдържа следните методи:

  • Конструктор, опционално приемащ хеш. Чрез него може да се зададат стойности на всеки от атрибутите за конкретната инстанция. Ключове, които не са зададени като атрибути се игнорират, а незададените атрибути са със стойност nil.
  • Метод #save, който записва или обновява записа в хранилището за данни (data_store-а). Ако в data_store-а вече съществува запис за обекта - обновяваме него, а не записваме дубликат.
  • По един getter и setter за всеки един атрибут.
  • Метод #delete - изтрива записа за текущата инстанция от хранилището (data store-а). Ако инстанцията не е записана - хвърля ексепшън DataModel::DeleteUnsavedRecordError.

Уникални идентификатори за записи

Добра практика е всеки запис в една база от данни да има уникален идентификатор. Това обикновено е полето id (съкратено от identifier), което присъства във всеки запис. Този атрибут съдържа число, уникално за всеки запис от конкретен тип.

Всеки модел трябва да съдържа атрибут id. Той не се задава експлицитно с attributes, но го има винаги.

ID-тата работят по следния начин:

  • При създаване на нов обект, id-то му е nil. User.new.id #=> nil
  • При записване на нов обект, id-то му се сетва на най-малкото положително цяло число (започва се от 1), което не е било използвано досега в хранилището на модела. Пример:
User.new.save.id #=> 1
User.new.save.id #=> 2
User.new.save.id #=> 3
  • id-то не се променя при повторно записване (обновяване) на един и същ обект.

Сравнения

Две инстанции на един и същ модел трябва да могат да се сравняват (чрез ==) по следния алгоритъм:

  • Необходимо (но не достатъчно) условие, за да са равни, е двата записа да са инстанции на един и същ модел.
  • Ако и двете инстанции са записани в data_store-а, считаме, че са равни точно тогава, когато ID-тата им са равни.
  • В противен случай две инстанции са равни само ако са един и същ обект в паметта.

Класови методи и търсене

Наследявайки от DataModel, трябва да получим следните класови методи за всеки модел:

  • .data_store - използва се за две цели:

    • За сетване на използваното хранилище за данни (вижте първия пример)
    • За достъп до вече зададено такова: User.data_store #=> #<ArrayStore ...>

    Подробно описание за data store ще намерите в секцията за хранилища по-надолу.

  • .attributes

    • Записва нов масив с атрибути за модела (отново първия пример)
    • Дава списък от символи, съдържащ имената на всички атрибути: Product.attributes #=> [:name, :price]
  • .where - позволява търсене на записи по определени полета:

User.new(name: 'Georgi').save
User.new(name: 'Georgi').save

User.where(name: 'Georgi') #=> [#<User ...>, #<User ...>]

Очакваме да можем да подадем повече от едно поле за търсене:

# ...create a few users...

User.where(name: 'Ivan', age: 34) #=> [#<User ...>]

В този случай трябва да намерим всички записи, които отговарят едновременно и на двете условия.

Ако бъде подадено поле, които не съществува за модела, трябва да се хвърли грешка DataModel::UnknownAttributeError със съобщение Unknown attribute <attribute_name>.

Finder-и по имената на атрибутите

За удобство, искаме нашите модели да разполагат с класови методи от вида .find_by_<attribute_name>, където <attribute_name> е името на дефиниран атрибут. Методите приемат по един аргумент - търсената стойност на атрибута. Както при where, връщат се всички записи, отговарящи на условието, като инстанции на модела.

Пример:

class User
  attributes :name, :age
  # ...
end

User.new(name: 'Georgi', age: 21).save

User.find_by_name('Georgi')             #=> [#<User ...>]
User.find_by_age(42)                    #=> []
User.find_by_email('gmail@georgi.com')  #=> NoMethodError

Хранилища за данни

Сигурно вече се чудите къде съхраняваме данните. Вместо база от данни, ще пазим всичко в паметта. За да сме по-гъвкави, ще направим така, че да можем да съхраняваме записи на различни места и по различни начини. Data store наричаме обект, който отговаря за самото съхраняване на записите.

Два такива data store-а, които трябва да имплементирате са ArrayStore и HashStore. Очевидно, от имената им, те имплементират записване съответно в масив и хеш.

Масивът (или хешът) трябва да се пазят като инстанционни променливи. Ключовете на хеша трябва да бъдат id-тата на съответните записи.

Двата вида хранилища имат един и същи интерфейс, като се различават единствено по имплементациите си. Така ще са напълно взаимозаменяеми. Това ни позволява, ако решим, да имплементираме нови начини за съхранение на данните - например FileStore, а защо не и SQLStore.

Този интерфейс се състои от следните четири CRUD метода:

  • #create - Приема хеш (запис) - атрибутите и стойностите на записа - и го добавя в колекцията.
  • #find - Приема хеш (заявка) - атрибутите и стойностите, по които търсим. Връща масив от хешове, отговарящи на заявката.
  • #update - Приема ID на обекта, който искаме да обновим, и хеш с атрибутите, които искаме да презапишем.
  • #delete - Приема заявка и изтрива всички обекти, отговарящи на заявката

Тези store-ове не трябва да знаят за съществуването на модели (DataModel). Всички методи тук приемат прости хешове, не инстанции на DataModel. Задача на самия модел е да преобразува инстанции от и към хешове преди да ги подаде на/вземе от съответния store.

ArrayStore и HashStore трябва да имат по един getter с име storage, съответно за масива и хеша, в който се съхраняват данните.

Бележки

  • В тази и бъдещи задачи ще считаме споделянето на тестове за преписване. Част от задачата е да се запознаете добре с условието и да се сетите за всички гранични случаи. Вече знаете как се пишат тестове - можете да си направите собствени. За преписването.
  • Като минимум си пуснете примерните тестове. Тях можете да ги използвате и като основа, върху която да напишете свои.