Project

General

Profile

Feature #16122

Updated by zverok (Victor Shepelev) over 1 year ago

**Value Object** is Currently, **Struct** behaves like a useful concept, introduced by Martin Fowler ([his post](https://martinfowler.com/bliki/ValueObject.html), [Wikipedia Entry](https://en.wikipedia.org/wiki/Value_object)) with the following properties (simplifying the idea): 

 *value object*: 
 * representing some relatively it is created from simple data; attributes; 
 * immutable; 
 * compared by type & value; 
 * nicely represented. its equality and representation based only on those attributes. 

 Value objects are super-useful especially for defining APIs, their input/return values. Recently, there were some movement towards using more immutability-friendly approach in Ruby programming, leading But also, it behaves like a **mutable object** (allows to creating several discussions/libraries with value objects. For example, [Tom Dalling's gem](https://github.com/tomdalling/value_semantics), [Good Ruby Value object convention](https://github.com/zverok/good-value-object) (disclaimer: the latter set attributes), and **as a kind-of collection** (includes `Enumerable`, has `to_a` and `[]` accessors). 

 And while `Struct` is maintained by yours truly). 

 kinda useful as is, I propose to introduce **native value objects** to Ruby as found that in a core class. 

 **Why not lot of cases what I really *mean* creating a gem?** 

 * I believe Struct is just creating a pure, immutable value object, that concept is *just it*. There are a lot of gems that simple, go this or that nobody *will even try* far to use a gem for representing it with, unless provide "value object" concept, but in fact, the framework/library used already provides one. 
 * Potentially, a lot of standard library (and probably even core) APIs could benefit from the concept. 

 **Why `Struct` concept is not enough** 

 Core `Struct` class is "somewhat alike" value-object, and frequently used instead of one: it is compared by value and consists of so simple attributes. On that typically nobody will install the other hand, `Struct` is: 
 * mutable; 
 * collection-alike (defines `to_a` and is `Enumerable`); 
 * dictionary-alike (has `[]` and `.values` methods). 

 The above traits somehow erodes the semantics, making code less clear, especially when duck-typing is used. 

 For example, this code snippet shows gem to just have it -- that's why `to_a` is problematic: I believe it should be in language core. 

 ```ruby 
 Result = Struct.new(:success, :content) 

 # Now, imagine that other code assumes `data` could be either Result, or [Result, Result, Result] 
 # So, ... 

 data = Result.new(true, 'it here is awesome') 

 Array(data) # => expected [Result(true, 'it is awesome')], got [true, 'it is awesome'] 

 # or... 
 def foo(arg1, arg2 = nil) 
 p arg1, arg2 
 end 

 foo(*data) # => expected [Result(true, 'it is awesome'), nil], got [true, 'it is awesome'] 
 ``` 

 Having `[]` and `each` defined on something that is thought as "just value" can also lead to subtle bugs, when some method checks "if the received argument is collection-alike", and value object's author doesn't thought of it as a collection. proposal **and its C implementation:** 

 **Concrete proposal** 

 * Class name: `Struct::Value`: lot `Struct::Value`, which shares most of Rubyists are used to have `Struct` as the implementation with a quick "something-like-value" drop-in, so alternative, more strict implementation, being part of `Struct` API, will be quite discoverable; *alternative: just `Value`* 
 * Class API (but is copying `Struct`s one (most neither subclass nor superclass of the time -- even reuses the implementation), with the following exceptions *(note: the immutability is **not** the only difference)*: 
   it); 
 * Unlike struct, it is:  
   * Not `Enumerable`; Enumerable; 
   * Immutable; 
   * Doesn't think of itself as "almost hash" (doesn't have `to_a`, `values` and `[]` methods); 
   * Can have empty members list (fun fact: `Struct.new('Foo')` creating member-less `Struct::Foo`, is allowed, but `Struct.new()` is not) to allow usage patterns like: 

 ```ruby 
 class MyService 
   Success = Struct::Value.new(:results) 
   NotFound = Struct::Value.new 
 end 
 ``` 

 
 `NotFound` here, unlike, say, `Object.new.freeze` (another pattern for creating "empty typed value object"), has nice inspect `#<value NotFound>`, and created consistently with the `Success`, making the code more readable. And if it will evolve to have some attributes, the code change would be easy. 

 **Patch is provided** 

 [Sample rendered RDoc documentation](https://zverok.github.io/ruby-rdoc/Struct-Value.html)

Back