Private features

by David Le Bansais (modified: 2011 May 03)

In Eiffel (to the best of my knowledge), a class B that inherits from a parent class A has access to all features of A. So far this hasn't bothered me at all, to the point that I often change my private functions in C# to protected.

But recently, as I was writing Eiffel programs to practice SCOOP, it occurred to me that I must now write a lot more features than I would before. Why is that? The reason comes from SCOOP controlled objects: what used to be simple qualified feature calls must now be turned into unqualified calls to 'private' routines. Let's see that with a quick example:

value_of_object_resource (some_object: SOME_CLASS): INTEGER local object_resource: RESOURCE do object_resource := some_object.get_resource result := object_resource.value end

With SCOOP, if some_object is separate, then object_resource has to be separate as well, which means it must be controlled before we can get its value:

value_of_object_resource (some_object: separate SOME_CLASS): INTEGER local object_resource: separate RESOURCE do object_resource := some_object.get_resource result := value_of_resource(object_resource) end value_of_resource (a_resource: separate RESOURCE): INTEGER do result := a_resource.value end

So far, so good. However, the requirement that objects must be controlled has the unintended effect of sometimes turning classes into a list of small, atomic features. This change does more than doubling the size of the source code, it also changes the design of the class by making a lot of internal calls available to descendants.

Let's take a look at {ENCODING}.convert_to:

feature -- Conversion convert_to (a_to_encoding: ENCODING; a_string: READABLE_STRING_GENERAL) -- Convert `a_string' from current encoding to `a_to_encoding'. -- If either current or `a_to_encoding' is not `is_valid', or an error occurs during conversion, -- `last_conversion_successful' is unset. -- Conversion result can be retrieved via `last_converted_string' or `last_converted_stream'. require a_to_encoding_not_void: a_to_encoding /= Void a_string_not_void: a_string /= Void local l_is_unicode_conversion: BOOLEAN l_unicode_conversion: like unicode_conversion do l_unicode_conversion := unicode_conversion if l_unicode_conversion.is_code_page_convertable (code_page, a_to_encoding.code_page) then encoding_i := l_unicode_conversion l_is_unicode_conversion := True else encoding_i := regular_encoding_imp end encoding_i.reset if l_is_unicode_conversion then encoding_i.convert_to (code_page, a_string, a_to_encoding.code_page) elseif a_to_encoding.is_valid and then is_valid and then is_conversion_possible (a_to_encoding) then encoding_i.convert_to (code_page, a_string, a_to_encoding.code_page) end end

If a_to_encoding is separate, then a new feature must be added to make a qualified call to a_to_encoding.code_page

... l_unicode_conversion.is_code_page_convertable (code_page, to_encoding_code_page(a_to_encoding)) ... to_encoding_code_page (a_to_encoding: separate ENCODING): like code_page do result := a_to_encoding.code_page end

My point is that, since the original version of ENCODING did not need this extra feature, there is probably no good reason to add it, from a design perspective. Therefore, SCOOP sort of forces you to design classes differently than what you would like to.

That would not be much of a problem (other than the obvious readability issue) if the new feature was private to the class, a possibility offered by several other languages but not in Eiffel. This sparked my curiosity, and I went through Object Oriented Software Construction, Eiffel, the language and Touch of Class. Alas, I couldn't find a good explanation of why to not allow features to be hidden to descendants. Can anyone come up with one?

Before SCOOP, if anyone wanted to expose a simple interface to clients but also to descendants, the option to write features made of big do...end blocks was there. This is no longer the case in the world of separate objects. It looks like this consequence was overlooked, and only the readability issue was acknowledged.

  • Manu (13 years ago 7/5/2011)

    This was not overlooked but it was felt as a small consequence for having a safe concurrent model. Nevertheless we are currently discussing the possibility of removing that needs in some cases, and providing a way to do separate calls in line.

    One example for the first case where it is not needed would be:

    s: separate OBJECT create s.make s.do_something

    It is clear in the above case that the current processor is the only one with access to the s' and thus there is no need to wrap the call for performing s.do_something'.

    For the second, we could have a way to make separate calls as in:

    s: separate OBJECT ... separate s.do_something

    which has the effect that the call had if you put the wrapper. The concern some people are raising is that it lacks the wait condition. Indeed with a wrapper you could do require s.is_ready do s.do_something end, therefore you still need a wrapper if the wait condition is critical for the logic. In additon, one has to remember that consecutive calls such as :

    separate s.do_something separate s.do_something

    are not guaranteed to be contiguous, since another call to `s' can be inserted between the two. To have this guarantee you still have to go through the wrapper.

  • David Le Bansais (13 years ago 7/5/2011)

    Currently I use (credits to Jocelyn for reminding me about inline agents)

    ;(agent (s_local: separate OBJECT) do s_local.do_something end).call([s])

    It's a bit verbose, but it does the job.

    If people want some wait logic, then a wrapper is required too.