{"id":15405708,"url":"https://github.com/zverok/good-value-object","last_synced_at":"2025-04-17T03:50:33.997Z","repository":{"id":46880085,"uuid":"124582568","full_name":"zverok/good-value-object","owner":"zverok","description":"Ruby Value Object conventions","archived":false,"fork":false,"pushed_at":"2020-07-21T17:05:43.000Z","size":15,"stargazers_count":94,"open_issues_count":3,"forks_count":1,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-03-29T05:51:18.263Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zverok.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-03-09T19:11:56.000Z","updated_at":"2025-03-19T18:40:14.000Z","dependencies_parsed_at":"2022-09-26T19:42:50.389Z","dependency_job_id":null,"html_url":"https://github.com/zverok/good-value-object","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fgood-value-object","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fgood-value-object/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fgood-value-object/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fgood-value-object/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zverok","download_url":"https://codeload.github.com/zverok/good-value-object/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249311792,"owners_count":21249314,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-01T16:18:20.006Z","updated_at":"2025-04-17T03:50:33.953Z","avatar_url":"https://github.com/zverok.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Good Value Object Conventions for Ruby\n\n\u003e In computer science, a **value object** is a small object that represents a _simple_ entity whose equality is not based on identity: i.e. two value objects are _equal_ when they have the same _value_, not necessarily being the _same object_. [→](https://en.wikipedia.org/wiki/Value_object)\n\nCreating good, reusable and idiomatic value objects for Ruby is not that simple.\n\n**This repository** provides a checklist for a good value object design. Currently it is in \"RFC\" (request for comments) state, gathering experience, agreements and convention. In future, it will also have _automated tests_, so you can just\n\n```ruby\n# in RSpec\nit_behaves_like \"good value object\",\n  arithmetic: false,\n  ordered: false,\n  sample_values: [\n    {lat: 1, lng: 2},\n    {lat: 50, lng: 40}\n    ...\n  ]\n```\n\n## Examples\n\nWe are using imaginary, yet real-life-alike `Quantity { amount: Numeric, unit: String }` type for most of the examples. And, eventually, other types that demonstrate some points better.\n\n## Definitions\n\nWe can think about most value objects as a Struct (not Ruby's particular implementation, but generic programming concept: group of named fields). The fields of this structure we further will call **structural elements**. It is logical concept rather than implementational.\n\n**Example**: for `Date` value type, \"structural\" values are probably `(year, month, day of month)` (maybe `calendar` too, depending of fanciness of your date). That does _not_ imply that `Date` instance stores them in instance variables, neither the fact that it is the only instance variables:\n\n* Date may be internally represented by one integer value, and calculate components back and force on construction and parts accessors;\n* Date may have _weekday_ as an accessor and instance variable. But it is probably derived value, because it indeed can be derived from year, month and day, and there are _almost_ no situations where it can be used to specify the date (e.g. `2018, March, Monday` is ambiguous, and `2018, March, 5th` doesn't need weekday to be specific).\n\n\u003e **Note**: `2018, 10th week, Monday` _is_ a thing in some business contexts, but probably it is better to have specialized constructor or even type for it.\n\n## Construction\n\n* `#initialize` should have type's structural parts as an arguments, the `TypeName.new(...)` should be the most straightforward (\"just validate and store in instance variables\") way to construct value; all other ways to construct should go to specialized class methods\n  ```ruby\n  # Bad\n  Quantity.new('10 m')\n  # Good\n  Quantity.new(10, 'm')\n  Quantity.parse('10 m')\n  ```\n  * See also \"[Conversions](#conversions)\"\" section\n  * Note: there are still cases where string representation is the most natural for \"default\" constructor:\n  ```ruby\n  # Probably OK\n  IPAddress.new('192.168.0.1')\n  # Does it bring additional clarity? YMMV\n  IPAddress.new(192, 168, 0, 1)\n  # Please don't!\n  IPAddress.new(byte1: 192, byte2: 168, byte3: 0, byte4: 1)\n  ```\n\n* It is acceptable to have structural elements converted or wrapped on construction\n  ```ruby\n  q = Quantity.new(10, 'm')\n  q.amount # =\u003e #\u003cBigDecimal 10\u003e\n  q.unit # =\u003e #\u003cQuantity::Unit m\u003e\n  ```\n\n* Prefer keyword arguments over positional ones in most cases, especially if there are more than 2 arguments for constructor, or order is not obvious (is it `GeoPoint.new(lat, lng)` or `GeoPoint.new(lng, lat)`?)\n  * (Obvious yet mandatory: please, use real keyword arguments, not pre-Ruby 2.1 `params = {}` hack)\n* Value construction options could be provided by keyword arguments, but it is undesirable to have both main argument and options as keyword arguments, or having both as positional arguments\n  ```ruby\n  # OK\n  Quantity.new(10, 'm')\n  Quantity.new(amount: 10, unit: 'm')\n  Quantity.new(10, 'm', system: Quantity::SI)\n  # Questionable\n  Quantity.new(amount: 10, unit: 'm', system: Quantity::SI)\n  Quantity.new(10, 'm', Quantity::SI)\n  ```\n  * Note: Probably, specialized constructors are better than options in the generic constructor;\n* Sometimes it is useful (but not required) to provide construction method synonymous with the type name, e.g. `Quantity(amount, unit)`; it brings no additional functionality yet emphasizes the fact that value \"just exists\", and we are referencing to existing concept of \"10 meters\", not constructing it (which \"new\" implies);\n  * As an alternative, consider providing `{Type}.call` class method: it allows to have almost the same look-and-feel, yet semantically belongs to the same module instead of being a global method:\n  ```ruby\n  # Shortcut for .call, available since Ruby 1.9\n  Quantity.(10, 'm')\n  # Some prefer this alternative:\n  Quantity[10, 'm']\n  ```\n* If there are expected to be a lot of similar objects created during the lifecycle of the application, consider caching objects (having exactly one object for one value). `Type.new` can be redefined for this purpose:\n  ```ruby\n  10.times.map { Quantity.new(10, 'm') }.map(\u0026:object_id).uniq.count # =\u003e 1\n  ```\n  Another approach seen in use is making `Type.new` private, and making `Type(...)` or `Type.[]` (with caching inside) the primary construction method.\n  * Consider global caching with a great thoroughness: it may be not threadsafe, and may lead to a lot of memory eaten if never gets flushed.\n\n* Avoid redefining `.new` for other purposes, especially to return value of type different from requested:\n  ```ruby\n  # Really bad\n  Quantity.new(10, 'm') # =\u003e #\u003cQuantity::Physics::Length 10 m\u003e\n  # Something like this would be better\n  Quantity.coerce(10, 'm') # =\u003e #\u003cQuantity::Physics::Length 10 m\u003e\n  # or even\n  Quantity['m'].new(10)\n  ```\n\n## Basic properties\n\n* All structural elements of the value should be exposed as `attr_reader`s (or methods with the same behavior)\n* Value object **should** be absolutely immutable, no `attr_writer`s and no other way to change value of the object\n  * It is wise to `freeze` all structural elements that belong to mutable Ruby types, to prevent code like this:\n    ```ruby\n    q = Quantity.new(10, 'm')\n    q.unit.upcase!\n    # Or, more believable:\n    q = Quantity.new(10, 'm')\n    u = q.unit\n    # ...later...\n    u.upcase! # =\u003e Unexpectedly makes q to have unit == 'M'\n    ```\n\n* As immutability makes this code impossible:\n  ```ruby\n  new_value = value.dup\n  new_value.property = x\n  ```\n  consider providing some _reasonable_ methods to \"produce a value like this, with some parts changed\"\n  * Consider (but mindfully) `merge(property: value, property: value)` interface for it\n    ```ruby\n    # Good\n    FancyDate.now.merge(month: 12) # produces new FancyDate: \"same day, but in December\"\n    # Not really useful\n    Quantity.new(10, 'm').merge(unit: 's') # what's the semantics of \"same value but in seconds\"?..\n    # Probably better\n    Quantity.new(10, 'm').unit.create(20) # =\u003e Quantity(20, 'm')\n    ```\n    * `#with` is another frequently used option instead of `#merge`.\n\n* **No global option** should change behavior of value objects. Consider providing \"context\" or \"environment\" to constructor or instance method:\n\n  ```ruby\n  # Unforgivable bad\n  Quantity.new(10, 'm').normalize # =\u003e Quantity.new(32.8, 'feet')\n  Quantity.system = Quantity::SI\n  Quantity.new(10, 'm').normalize # =\u003e Quantity.new(10, 'm')\n\n  # Still pretty questionable\n  Quantity.new(10, 'm').normalize # =\u003e Quantity.new(32.8, 'feet')\n  Quantity.new(10, 'm', system: Quantity::SI).normalize # =\u003e Quantity.new(10, 'm')\n\n  # Good\n  Quantity.new(10, 'm').normalize # =\u003e Quantity.new(32.8, 'feet')\n  Quantity.new(10, 'm').normalize(system: Quantity::SI) # =\u003e Quantity.new(10, 'm')\n\n  # Best ;)\n  Quantity.new(10, 'm').normalize # =\u003e Quantity.new(10, 'm')\n  Quantity.new(10, 'm').normalize(system: Quantity::IMPERIAL) # =\u003e Quantity.new(32.8, 'feet')\n  ```\n\n### `#inspect` and `#pp`\n\n* You should implement `#inspect` for your types, it is really helpful for debugging\n* By convention, `#inspect` for value types should look like `#\u003cTypeName value representation\u003e`\n* Value representation should be full (without losing important details) yet concise (without variable names and unimportant clarifications)\n  ```ruby\n  # Good\n  Quantity.new(10, 'm').inspect # =\u003e \"#\u003cQuantity 10 m\u003e\" or #\u003cQuantity(10 m)\u003e`\n  # Bad\n  Quantity.new(10, 'm').inspect # =\u003e \"#\u003cQuantity(m)\u003e\"\n  Quantity.new(10, 'm').inspect # =\u003e \"10 m\" - it is unhelpful to not be able to distinguish from string while debugging\n  Quantity.new(10, 'm').inspect # =\u003e \"#\u003cQuantity amount=10 unit=\\\"m\\\"\u003e\" - unnecessary verbosity\n  # Also bad: Ruby's stdlib Date\n  Date.today.inspect # =\u003e \"#\u003cDate: 2018-03-04 ((2458182j,0s,0n),+0s,2299161j)\u003e\" -- ((2458182j,0s,0n),+0s,2299161j) anybody?\n  ```\n* If it can be created, it **should** be possible to inspect; `#inspect` should try hard to never raise and never return anything except string\n  ```ruby\n  # Good\n  Quantity.new(INFINITY, 'm') # =\u003e ArgumentError on attempt to create, no problems with inspect\n  # Acceptable\n  Quantity.new(INFINITY, 'm').inspect # =\u003e \"#\u003cQuantity [UNREPRESENTABLE]\u003e\"\n  # Bad\n  Quantity.new(INFINITY, 'm').inspect # =\u003e ArgumentError or nil\n  ```\n* If it is known beforehand about some possible basic values the value object will try to represent, it is advisable to try providing nicer inspects, immediately readable\n  ```ruby\n  # Not really helpful\n  Quantity.new(10_000_000, 'm').inspect # =\u003e #\u003cQuantity 10000000 m\u003e\n  # Good\n  Quantity.new(10_000_000, 'm').inspect # =\u003e #\u003cQuantity 10,000,000 m\u003e\n  # Could be acceptable in some contexts\n  Quantity.new(10_000_000, 'm').inspect # =\u003e #\u003cQuantity 1e7 m\u003e\n  ```\n* As since Ruby 2.5 `pp` is required by default, consider implementing multiline `#pretty_print` for the value, especially if it contains lots of data that is reasonable to print in multiple lines\n  * Documentation on implementing `#pretty_print` (pretty terse, yet enough to start) could be found [here](https://docs.ruby-lang.org/en/2.5.0/PP.html#class-PP-label-Output+Customization)\n\n## Comparison\n\n* Provide `==` method for values\n  * Values should be equal if, and only if, all of their structural elements are equal _or could be converted in a tuple of equal structured elements_\n  ```ruby\n  # example of the latter:\n  Quantity.new(1000, 'm') == Quantity.new(1, 'km') # =\u003e probably true, unless the domain is some formal reporting system\n  ```\n  * `==` should NOT raise on attempt to compare with incompatible type: in Ruby, `1 == \"1\"` is just `false`, not a deadly sin punished by exception\n  * Value of other type _could_ be considered equal, if it could be converted into value of current type without loosing context:\n  ```ruby\n  # Good\n  Quantity.new(1, 'm') == Unitwise(1, 'm')\n  Date.parse('2017-05-01') == Time.parse('2017-05-01') # Doesn't work in Ruby though ;)\n  # Bad\n  Quantity.new(10, 'm') == 10 # could be helpful in some particular case yet source of hidden bugs\n  ```\n* See \"[Behavior in hashes](https://github.com/zverok/good-value-object#behavior-in-hashes)\" about overriding `#eql?`\n* **Never** override `#equal?`\n* Provide order comparison for values (`\u003c`, `\u003e` and so on) if, and only if, order on all acceptable values is defined and unambiguous\n  * It is strongly advised to provide those methods by implementing `\u003c=\u003e` and including `Comparable` (and it will give you `==` for free)\n  * `\u003c=\u003e` should NOT raise on attempt to compare with incompatible type, just return `nil`, `Comparable`s implementation of other method will behave the most reasonable way: `==` will return `false` and `\u003c` and other similar methods would raise `ArgumentError`\n  * if implementing `\u003c` and `\u003e` by yourself, don't forget about `\u003c=` and `\u003e=`; and make them raise `ArgumentError` on incompatible types\n* Consider providing `positive?`, `negative?` and `zero?` for the value if, and only if, their meaning is clear and semantically unambiguous\n* If the order on values is strictly defined, consider providing `Type::INFINITY` constant or class method, for using in expressions like:\n  ```ruby\n  ranges = {\n    Quantity.new(1, 'm')...Quantity.new(10, 'm') =\u003e 'near',\n    Quantity.new(10, 'm')...Quantity.new(100, 'm') =\u003e 'far',\n    Quantity.new(100, 'm')...Quantity::INFINITY =\u003e 'nowhere'\n  }\n  ranges.select { |r, _| r.cover?(value) }....\n  # and this\n  value.clamp(Quantity.new(100, 'm'), Quantity::INFINITY) # \"not lower the 100\" one-side clamp\n  ```\n  Possible infinity concept interfaces:\n  ```ruby\n  # Probably OK if used rarely, and constructor should not fail on this\n  Quantity.new(Float::INFINITY, 'm')\n  # Pretty clear yet no explicit type, can be hard to implement \u003c=\u003e\n  Quantity::INFINITY\n  # Also clear and typed, needs mindful implementation\n  Quantity.infinity('m')\n  ```\n  * See also \"[Behavior in ranges](https://github.com/zverok/good-value-object#behavior-in-ranges)\" for notes about Range implementation quirks\n\n## Other operators\n\n* Consider providing a subset of math operators (`+`, `-`, `*`, `/` and so on) if their meaning is obvious and unambiguous\n* Try to follow \"natural\" intuition of mathematical operators (`a + b == b + a`, `a - b = a + (-b)` and so on)\n  * Note that Ruby's intuition also redefines some of operators base qualities, when acceptable, for example, using `+` for _concatenation_ (of strings and arrays), which is not commutative\n* Don't override operators just because it is cool: using, say `~Quantity.new(10, 'm')` to say \"something about this quantity\" (for example, producing range `Quantity.new(9.5, 'm')..Quantity.new(10.5, 'm')`) is witty yet leads to unguessable code\n* Consider implementing `|` and `\u0026` if:\n  * value object is some kind of pattern, for this operators to mean \"or\" and \"and\"\n  * value object represents some kind of range(s), for this operators to mean \"union\" and \"intersection\"\n  ```ruby\n  Dates::Period.parse('2017-02') | Dates::Period.parse('2016-12')\n  # =\u003e #\u003cDates::Period Dec 1-31 2016, Feb 1-28 2017\u003e\n  Dates::Period.parse_range('2017-01-30'..'2017-02-12') \u0026 Dates::Period.parse('2017-01')\n  # =\u003e #\u003cDates::Period Jan 30-31 2017\u003e\n  ```\n* Consider implementing `===` if value can be used as some kind of pattern\n  ```ruby\n  # Messy\n  if quantity.unit == 'm'\n  elsif quantity.unit == 's'\n  else ...\n\n  quantities.select { |q| q.unit == 'm' }\n\n  # Nice\n  case quantity\n  when Quantity::Unit('m')\n  when Quantity::Unit('s')\n  ...\n\n  quantities.grep(Quantity::Unit('m'))\n  ```\n\n## Conversions\n\n### To other types\n\n* Consider providing `#to_\u003ctype\u003e` to convert value object to other types\n* `#to_\u003ctype\u003e` protocol should be used only when format or precision of value is changed, but not when context is lost\n  ```ruby\n  # Good\n  BigDecimal('100').to_i # =\u003e it is the same number, just loses precision\n  # Bad\n  Quantity.new(10, 'm').to_i # =\u003e context is lost, Quantity#amount is much better convention\n\n  # Acceptable\n  Dates::Period.to_activercord # =\u003e may have sense in some context\n  # Questionable\n  Dates::Period.to_regexp # =\u003e probably, just #regexp would be better\n  ```\n\n### To Ruby's core types\n\n* Never provide \"implicit conversion\" methods (`#to_str`, `#to_ary`, `#to_hash`, `#to_int`) unless you really know what you do (= type is really kind of string/array/hash/integer); they'll convert values violently and unexpectedly;\n* Never provide `to_a` either (unless it is kind of collection), as it will unexpectedly deconstruct the value on `Array(value)` call\n  * This means that if the type is descendant of `Struct`, you **should explicitly** `undef :to_a`\n  * Even for objects \"somewhat resembling collection\", it is better to provide one or more `#each_\u003csomething\u003e` methods, returning `Enumerator`\n* Always try to provide `#to_h`, it is really good for serialization:\n  * `#to_h` should probably return hash with symbolic keys, containing exactly all the structural elements of value object and nothing more;\n  * If value object's constructor uses keyword arguments, `ValueType.new(**value.to_h) == value` should be always true\n* Always provide `#to_s`, as Ruby's default `#to_s` will expose object_id and look really unhelpful on string interpolations\n  * for value objects that represent typed values (time, geometry, quantities) consider providing as \"human-readable\" `#to_s` as possible, without any quoting and type names;\n  * for value objects that represent complicated domain structures, consider making `#to_s` just an alias to `#inspect` (see \"`#inspect` and `#pp`\" section above)\n  ```ruby\n  # Good\n  puts Quantity.new(10 'm') # \"10 m\"\n  # Also good\n  puts StoreId.fetch('xyz') # =\u003e \"#\u003cStoreId xyz\u003e\"\n  # Questionable\n  puts StoreId.fetch('xyz') # =\u003e \"xyz\" -- Loses too much of domain context\n  # if you needed this to interpolate sql, probably #to_sql method would be better\n  ```\n  * If there are a lot of way to represent value as a string, consider providing `#format(lot: of, **options)` or `strf\u003ctypename\u003e`\n\n### From other types\n\n* Consider providing `Type.from_\u003cothertype\u003e()` methods for as much of basic Ruby types, and domain types, as possible;\n* As with `to_\u003cothertype\u003e`, the `from_` naming convention can ONLY be used if format or precision of data is changed, but not when context is lost or attached:\n```ruby\n# Good\nQuantity.from_a([10, 'm']) # =\u003e #\u003cQuantity 10 m\u003e\n# Bad\nQuantity.from_f(10, unit: 'm') # It is constructor (maybe specialized one), not \"converter from Float\"!\n```\n* Most of the time, `Type.from_othertype(value.to_othertype) == value` should be `true`;\n* Sometimes, it is useful to provide two methods for conversion: one raising on incorrect input, and other just returning `nil`:\n```ruby\nQuantity.from_a([10, 'm']) # =\u003e #\u003cQuantity 10 m\u003e\nQuantity.from_a([10]) # =\u003e ArgumentError: expected 2-element array\nQuantity.try_from_a([10]) # =\u003e nil\n```\n* If the domain data can have very variable string representation, consider providing two ways to parse:\n  * `Typename.parse(string)` that accepts any input, tries to guess how to parse it, and returns `nil` if it absolutely can not;\n  * Set of methods, or set of options, or pattern DSL allowing user to specify how data should be parsed:\n  ```ruby\n  # set of methods:\n  Quantity.amount_unit('10m') # =\u003e #\u003cQuantity 10 m\u003e\n  Quantity.unit_amount('$10') # =\u003e #\u003cQuantity 10 $\u003e\n  # set of options\n  Quantity.from_s('$\u0026nbsp;10', order: :unit_amount, separator: '\u0026nbsp;')\n  # pattern DSL\n  Quantity.strpquantity('%amount (%unit)', '20 (m)')\n  ```\n  Note: `strp\u003ctypename\u003e` is probably not the best convention, but it is like Ruby's `Date.strptime`\n\n## Behavior in hashes\n\n* If it is _a slightest possibility_ the value type could be used as a key in hashes, implement `#hash`, returning different number for different combinations of structural elements, and same number for same combination. The easiest implementation is probably\n  ```ruby\n  def hash\n    [each, of, structural, elements, self.class].hash\n  end\n  ```\n* In this case `#eql?` method also **should** be implemented, as Hash uses it to decide on key's equality on `#hash` values collision (as number of possible integer values could be lower than number of possible value object values). Typically, it can be just an alias to `#==`, but if `#==` is forgiving, `#eql?` should be strict.\n  ```ruby\n  # Imagine Paragraph class, which is just a wrapper around String, but with some fancy interface\n  # It can have...\n  def ==(other)\n    @string == other.to_s\n  end\n\n  # In this case...\n  h = {'test' =\u003e 1, Paragraph.new('test') =\u003e 2}\n  # ...may lead to only ONE key being stored\n  ```\n* If `#eql?` implementation is different from `#==`'s, **never** implement it as a `#hash` comparison\n  ```ruby\n  # Really bad: on hash collision Hash will have no means of telling two values one from other\n  def eql?(other)\n    hash == other.hash\n  end\n  ```\n\n## Behavior in ranges\n\nFor most of its functionality, Ruby's `Range` currently relies on value providing `#succ` (next value in ordered values space). Unfortunately, this includes case equality `===` too.\n\n**UPD:** Since Ruby 2.6, `#===` uses `cover?` underneath, so \"not working\" or \"too slow\" examples of `case` below are working correctly. So, rule about implementing `#succ` becomes simpler: \"Implement it, if it makes unambigous sense for your type\".\n\nFor code expecting to work under Ruby \u003c 2.6, there stay two opposite rules:\n\n* Consider providing `#succ` method if value space is small and has unambiguous granularity, to allow code like this:\n  ```ruby\n  case DayOfWeek.current\n  when DayOfWeek('Mon')..DayOfWeek('Thu')\n  ```\n* Consider consciously NOT providing `#succ` to explicitly disallow code like this:\n  ```ruby\n  # Idiomatic, yet slow: calculates thousands of IPs inside range\n  case ip\n  when IP(\"172.16.10.1\")..IP(\"172.16.11.255\")\n  ...\n\n  # Can't be used in `case`, yet fast:\n  if (IP(\"172.16.10.1\")..IP(\"172.16.11.255\")).cover?(ip) ...\n\n  # Another case\n  #\n  # Ruby will try to do #succ on start value, but what it should be?\n  # \"Obvious\" from the first sight Quantity.new(2, 'm') will leave Quantity.new(1.5, 'm') outside the comparison\n  case quantity\n  when Quantity.new(1, 'm')..Quantity.new(10, 'm')\n  ...\n\n  # The only solution, again:\n  if (Quantity.new(1, 'm')..Quantity.new(10, 'm')).cover?(quantity)\n  ```\n\n## Serialization/deserialization\n\n* Consider providing reasonable `#to_json` implementation. For lot of cases, this should be enough (if you have provided `#to_h` which is strongly advised above):\n  ```ruby\n  def to_json(*opts)\n    to_h.to_json(*opts)\n  end\n  ```\n* Consider your value object's YAML-friendliness\n  * Default YAML implementation will dump all object's instance variables on `YAML.dump`, and just set them all to an uninitialized allocated object on `YAML.load`. You can alter this behavior by redefining methods with inventive and memoizable names `encode_with(coder)` and `init_with(coder)`\n  ```yaml\n  # good\n  - !ruby/object:Quantity\n    amount: 1\n    unit: m\n\n  # not so good\n  - !ruby/object:Quantity\n    amount: 1\n    unit: !ruby/object:Quantity::Unit\n      name: meter\n      synonym: metre\n      plural: metres\n      short: m\n      domain: distance\n      base: true\n      system: !ruby/object:Quantity::System\n        ...\n    _memoized_method_cache_: # memoist was here....\n    ...\n  ```\n\n## Inheritance friendliness\n\nFor small value objects it is always a temptation to inherit from, to add several more methods, change constructor or formatting, required by current domain. Your types should be ready to be inherited, which most of the time, means not hardcoding class (by name or by value) in methods (or, sometimes, vice versa, hardcoding it, look at examples)\n\n```ruby\nclass FancyQuantity \u003c Quantity\nend\n\n# Bad\nFancyQuantity.new(10, 'm').inspect # =\u003e #\u003cQuantity 10 m\u003e, because #inspect hardcodes \"#\u003cQuantity\" part\n# solution is\ndef inspect\n  \"#\u003c#{self.class} .... \u003e\"\nend\n\n# Probably bad\nFancyQuantity.new(10, 'm') == Quantity.new(10, 'm') # =\u003e false, because #== has self.class == other.class\n\n# solution?\ndef ==(other)\n  # Bad: only values of exactly same type are compatible\n  self.class == other.class \u0026\u0026 ...\n  # Bad: Quantity#==(FancyQuantity) would work, but not vice versa\n  other.kind_of?(self.class) \u0026\u0026 ...\n\n  # Good: just hardcode the base\n  other.is_a?(Quantity) \u0026\u0026 ...\n  # ...or, sometimes, duck type\n  other.respond_to?(:amount) \u0026\u0026 other.respond_to?(:unit) \u0026\u0026 ...\nend\n```\n\n## Pattern Matching\n\nValue objects are a prime candidate for use with the pattern matching syntax introduced in Ruby 2.7.\n\nThe easiest way to integrate with pattern matching is to return a hash of attributes from `#decontruct_keys`. The simplest implementation is to just call `#to_h`, if that method is already available. This allows the object to be pattern matched using the hash syntax:\n\n```ruby\nclass Quantity\n  ...\n\n  def decontruct_keys(_keys)\n    to_h\n  end\nend\n\ncase Quantity.new(10, 'm')\nin unit: 'm', amount:\n  puts \"Metric: #{amount} meters\"\nin unit: 'ft', amount:\n  puts \"Imperial: #{amount} feet\"\nend\n```\n\nThe `_keys` argument provided to `#decontruct_keys` can be ignored, but if generating the returned hash is an expensive operation, this argument can be used to optimize performance.\n\nConsider also supporting the array syntax if the attributes have an obvious or intuitive ordering (See also: keyword arguments vs positional arguments in [Construction](#construction)). This is done by returning an array of attributes from the `#deconstruct` method:\n\n```ruby\nclass Quantity\n  ...\n\n  def deconstruct\n    [@amount, @unit]\n  end\nend\n\ncase Quantity.new(10, 'm')\nin amount, 'm'\n  puts \"Metric: #{amount} meters\"\nin amount, 'ft'\n  puts \"Imperial: #{amount} feet\"\nend\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Fgood-value-object","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzverok%2Fgood-value-object","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Fgood-value-object/lists"}