📜 ⬆️ ⬇️

Design Scent: Temporary Connectivity

This is the first post in the Poka-yoke design series - also known as encapsulation .

A well-known problem in designing an API is the temporal connectivity , which is obtained if the class has hidden relationships between two or more members that require the correct call sequence from the client. It tightly binds class members in time section.

An archetypical example is the use of the Initialize method, although many more examples can be found, including in the BCL (FCL). As an example from BCL, the following use of the EndpointAddressBuilder class is compiled, but crashes at runtime:
var b = new EndpointAddressBuilder(); var e = b.ToEndpointAddress(); 

It turns out that to construct an EndpointAddress, you must at least provide a URI. The following example is compiled and normally executed:
 var b = new EndpointAddressBuilder(); b.Uri = new UriBuilder().Uri; var e = b.ToEndpointAddress(); 

The API does not give any hints that the use of a URI is necessary. Here a temporary connection appears between setting the URI property and calling the ToEndpointAddress method.
Next, we will look at a more complete example, and I will give a guide to improve the API in the direction of Poka-yoke.

An example of "smell."
This example describes a more abstract "smell", shown in the Smell class. An open API might look like this:
 public class Smell { public void Initialize(string name) public string Spread() } 

Semantically, the name of the Initialize method is the key to a solution, but at the structural level this API does not indicate the presence of a temporal connectivity. Thus, the following code is compiled, but crashes during execution:
 var s = new Smell(); var n = s.Spread(); 

It turns out that the Spread method throws an InvalidOperationException because the Smell object was not initialized with the name. The problem with the Smell class is that it does not properly protect its invariants. In other words, encapsulation is broken.
To solve the problem, the Initialize method must be called before calling the Spread method:
 var sut = new Smell(); sut.Initialize("   "); var n = sut.Spread(); 

In the case where it is possible to write a unit test, with which you can study the behavior of the Smell class, it would be much better if the design allowed to receive feedback at the compilation stage.
')
Fix: injection through constructor
Encapsulation requires that the class never be in a contradictory state. Given that the name is necessary for the Smell class, a guarantee of its presence must be built into the class. If it is impossible to provide a name with a default value, then the name should be requested in the constructor of the Smell class:
 public class Fragrance : IFragrance { private readonly string name; public Fragrance(string name) { if (name == null) { throw new ArgumentNullException("name"); } this.name = name; } public string Spread() { return this.name; } } 

This is an effective guarantee that the name is always available in all instances of the class. Other positive effects are also present here:

However, sometimes it happens that the original version of the class implements the interface that is the cause of the temporal connectivity. Here is an example:
 public interface ISmell { void Initialize(string name); string Spread(); } 

In many cases, the injected value remains unknown until runtime, and in this case, the straightforward use of the constructor seems somewhat prohibiting — after all, the constructor is an implementation detail, and not part of a loosely coupled API . When you program at the interface level, you cannot call the constructor.
A solution to this problem also exists.

Fix: Abstract Factory
To separate the methods in the ISmell interface (haha), the Initialize method can be moved to a new interface. Instead of changing the state of the (inconsistent) class, the Create (formerly Initialize) method returns a new instance of the IFragrance interface:
 public interface IFragranceFactory { IFragrance Create(string name); } 

Simple implementation:
 public class FragranceFactory : IFragranceFactory { public IFragrance Create(string name) { if (name == null) { throw new ArgumentNullException("name"); } return new Fragrance(name); } } 

This provides encapsulation, since both classes - FragranceFactory and Fragrance protect their invariants. They can never be in a contradictory state. A client that previously interacted with the ISmell interface can now use the IFragranceFactory / IFragrance combination to get the same functionality:
 var f = factory.Create(name); var n = f.Spread(); 

This is much better, since the misuse of the API will now be determined at compile time, not at run time. An interesting side effect as we move towards a statically declared interaction structure is that classes tend to be immutable. Immutable classes automatically become thread-safe, which is an increasingly important quality in the new (relatively) multi-core era.

Source: https://habr.com/ru/post/205114/


All Articles