Search code examples
rubyumlclass-diagram

What is the relation in (class diagrams) between those 3 classes?


I have the code as follow :

class Synchronization

  def initialize

  end
  
  def perform
    detect_outdated_documents
    update_documents
  end

  private

  attr_reader :documents


  def detect_outdated_documents
    @documents = DetectOutdatedDocument.new.perform
  end

  def update_documents
    UpdateOutdatedDocument.new(documents).perform
  end

@documents is an array of Hashes I return from a method in DetectOutdatedDocument. I then use this array of Hash to initialize the UpdateOutdatedDocument class and run the perform method.

Is something like this correct? Or should I use associations or something else?

class diagrams


Solution

  • Ruby to UML mapping

    I'm not a Ruby expert, but what I understand from your snippet given its syntax is:

    • There's a Ruby class Synchronization: That's one UML class
    • The Ruby class has 4 methods initialize, perform, detect_outdated_documents, and update_documents, the two last being private. These would be 4 UML operations.
    • initialize is the constructor, and since it's empty, you have not mentioned it in your UML class diagram, and that's ok.
    • The Ruby class has 1 instance variable @documents. In UML, that would be a property, or a role of an association end.
    • The Ruby class has a getter created with attr_reader. But since it is in a private section, its visibility should be -. This other answer explains how to work with getters and setters elegantly and accurately in UML (big thanks to @engineersmnky for the explanations on getters in Ruby, and for having corrected my initial misunderstanding in this regard)
    • I understand that SomeClass.new creates in Ruby a new object of class SomeClass.

    Ruby and dynamic typing in UML

    UML class diagrams are based on well-defined types/classes. You would normally indicate associations, aggregations and compositions only with known classes with whom there’s for sure a stable relation. Ruby is dynamically typed, and all what is known for sure about an instance variable is that it's of type Object, the highest generalization possible in Ruby.

    Moreover, Ruby methods return the value of the latest statement/expression in its execution path. If you did not care about a return value of an object, you'd just mark it as being Object (Thanks engineersmnky for the explanation).

    Additional remarks:

    • There is no void type in UML (see also this SO question). An UML operation that does not return anything, would just be an operation with no return type indicated.
    • Keep also in mind that the use of types that do not belong to the UML standard (such as Array, Hash, Object, ...) would suppose the use of a language specific UML profile.

    Based on all this, and considering that an array is also an Object, your code would lead to a very simple UML diagram, with 3 classes, that are all specializations of Object, and a one-to-many association between Synchronization and Object, with the role @documents at the Object end.

    Is it all what we can hope for?

    The very general class diagram, may perhaps match very well the implementation. But it might not accurately represent the design.

    It's your right to model in UML a design independently of the implementation. Hence, if the types of instance variables are known by design (e.g. you want it to be of some type and make sure via the initialization and the API design that the type will be enforced), you may well show this in your diagram even if it deviates from the code:

    • You have done some manual type inferencing to deduce the return type of the UML operations. Since all Ruby methods return something, we'd expect for all Ruby methods at least an Object return type. But it would be ok for you not to indicate any return type (the UML equivalent to void) to express taht the return value is not important.
    • You also have done some type inference for the instance variable (UML property): you clarify that the only value it can take is the value return by DetectOutdatedDocument.new.perform.
    • Your diagram indicates that the class is related to an unspecified number of DetectOutdatedDocument objects, and we guess it's becaus of the possible values of @documents. And the property is indicated as an array of objects. It's very misleading to have both on the diagram. So I recommend to remove the document property. Instead, prefer a document role at the association end on the side of DetectOutdatedDocument. This would greatly clarify for the non-Ruby-native readers why there is a second class on the diagram. :-) (It took me a while)
    • Now you should not use the black diamond for composition. Because documents has a public reader; so other objects could also be assigned to the same documents. Since Ruby seems to have reference semantic for objects, the copy would then refer to the same objects. That's shared aggregation (white diamond) at best. And since UML has not defined very well the aggregation semantic, you could even show a simple association.

    A last remark: from the code you show, we cannot confirm that there is an aggregation between UpdateOutdatedDocument and DetectOutdatedDocument. If you are sure there is such a relationship, you may keep it. But if it's only based on the snippet you showed us, remove the aggregation relation. You could at best show a usage dependency. But normally in UML you would not show such a dependency if it is about the body of a method, since the operation could be implemented very differently without being obliged to have this dependency.