Design Fluent API in C #
Some frameworks we often use, such as EF, Automaper, and nhib.pdf, provide excellent Fluent APIs. Such APIs fully utilize the smart prompts of, the written code is also very neat. How can we also write this Fluent code in the code? Here I will introduce 3 commonly used models, A slight change or modification in these modes can be used as an API that can be used in actual projects. Of course, without the need to design an API, it is also very helpful for us to understand the code of other frameworks. 1. The simplest and most practical design this is the most common and simplest design. Each method returns return this internally, so that all methods of the entire class can be written in a series. The Code is also very simple: it is also very simple to use: public class CircusPerformer {public List <string> PlayedItem {get; private set;} public CircusPerformer () {PlayedItem = new List <string> ();} public CircusPerformer StartShow () {// make a speech and start to show return this;} public CircusPerformer MonkeysPlay () {// monkeys do some show PlayedItem. add ("MonkeyPlay"); return this;} public CircusPerformer ElephantsPlay () {// e Lephants do some show PlayedItem. add ("ElephantPlay"); return this;} public CircusPerformer TogetherPlay () {// all of the animals do some show PlayedItem. add ("TogetherPlay"); return this;} public void EndShow () {// finish the show} call: [Test] public void All_shows_can_invoke_by_fluent_way () {// Arrange var circusPerformer = new CircusPerformer (); // Act circusPerformer. monkeysPlay (). elephantsPla Y (). startShow (). togetherPlay (). endShow (); // Assert circusPerformer. playedItem. count. shocould (). be (3); circusPerformer. playedItem. contains ("MonkeysPlay"); circusPerformer. playedItem. contains ("ElephantsPlay"); circusPerformer. playedItem. contains ("TogetherPlay");} but this API has a flaw. circusPerformer has a sequence during the performances. You must first call StartShow () and then perform various performances, after the show ends, you must call EndShow () to end the show. However, obviously such an API cannot meet such requirements. You can change the call sequence as you like. We know that as an excellent API, we should try to avoid making mistakes to users. For example, designing private fields and readonly fields will prevent users from modifying internal data and causing unexpected results. 2. Design a Fluent API with the call sequence. In the previous example, the API designer expects the user to first call the StartShow () method to initialize some data and then perform the performance, finally, the user can call EndShow (). The idea is to abstract different types of functions into different interfaces or abstract classes. return this is not used in the method, instead of return INext; based on this idea, we abstract StartShow () and EndShow () methods into a class, and abstract circus performances into an interface: public abstract class extends mer {public abstract IList <string> PlayedItem {get; protected set;} public abstract ICircusPlayer StartShow (); public abstract void EndShow ();} p Ublic interface ICircusPlayer {ICircusPlayer MonkeysPlay (); ICircusPlayer ElephantsPlay (); ICircusPlayer TogetherPlay ();} with this classification, we redesign the API: public class CircusPerfomer: timer, Mer, ICircusPlayer {public override sealed IList <string> PlayedItem {get; protected set;} public CircusPerfomer () {PlayedItem = new List <string> ();} public override ICircusPlayer StartShow () {// make a speech and start Show return this;} public ICircusPlayer MonkeysPlay () {// monkeys do some show PlayedItem. add ("MonkeyPlay"); return this;} public ICircusPlayer ElephantsPlay () {// elephants do some show PlayedItem. add ("ElephantPlay"); return this;} public ICircusPlayer TogetherPlay () {// all of the animals do some show PlayedItem. add ("TogetherPlay"); return this;} public override void EndShow () {// finish th E show} can meet our requirements. On the circusPerformer instance, only StartShow () and EndShow () can be called. After StartShow () is called, various performance methods can be called. Of course, because our API is very simple, this design is still justified. if the business is complicated, we need to consider a large number of situations or orders that we can further improve, the basic idea of implementation is to use the modifier mode and extension method, because dax.net in the garden published related blogs a long time ago and used the decorator mode and Extension Method in C # To implement the Fluent Interface, you can refer to the implementation solution of this article, this design can be said to be the ultimate mode, and the implementation process is also complicated. III. The Fluent Design of generic classes has a non-problematic problem. That is, generic parameters cannot be omitted, when using a type such as var list = new List <string> (), you must specify the exact string type. In comparison, the types in generic methods can be omitted. the compiler can deduce the parameter types based on the parameters, for example, var circusPerfomer = new CircusPerfomerWithGenericMethod (); circusPerfomer. show <Dog> (new Dog (); circusPerfomer. show (new Dog (); is there a way to omit the type in the generic class? The answer is: one elegant way is to introduce a non-generic static class. A static generic method is implemented in a static class, and the method returns a generic type. This sentence is very difficult. Let's take a look at an image board instance. Define a Drawing <TShape> class. This class can plot a pattern of the TShape type. public class Drawing <TShape> where TShape: IShape {public TShape Shape {get; private set ;} public TShape Draw (TShape shape) {// drawing this shape Shape = shape; return shape ;}} defines a Canvas class. this class can be used to Draw Pig based on the input basic shape, call the corresponding Drawing <TShape> to combine a Pig to public void DrawPig (Circle head, Rectangle mouth) {_ history. clear (); // use generic class, complier can not infer th E correct type according to parameters Register (new Drawing <Circle> (). draw (head), new Drawing <Rectangle> (). draw (mouth);} the Code itself is very understandable and clean. If we want to use the techniques we mentioned earlier to implement a method that omitting the generic type and is Fluent, we can design it as follows: First, such a design should rely on a static class: public static class Drawer {public static Drawing <TShape> For <TShape> (TShape shape) where TShape: IShape {return new Drawing <TShape> ();}} then draw a Dog public void DrawDog (Circle head, Rectangle mouth) {_ history using this static class. clear (); // fluent implements Register (Drawer. for (head ). draw (head), Drawer. for (mouth ). draw (mouth);} You can see that it has become A Method of Fluent writing, which is also relatively clean. Here I come up with a saying, "What is the effort of paying?", which is what many people want to say here, I can only say that you can regard this as an amazing skill. If the framework you use has such an API one day, you can understand what is going on. 4. I would like to give an example to illustrate this kind of technique which is frequently used in some cases. When writing EF configurations and Automaper configurations, we often write as follows: xx. mapPath (Path. for (_ student ). property (x => x. name), Path. for (_ student ). property (x => x. email), Path. for (_ customer ). property (x => x. name), Path. for (_ customer ). property (x => x. email), Path. for (_ manager ). property (x => x. name), Path. for (_ manager ). property (x => x. email) This method is changed from the previous technique. Now we design a Validator. If this Validator needs to batch input the Model fields For line verification, we also need to define a configuration file, how should we configure the XX field of the XX Model? With this configuration, we can verify which data does not conform to this configuration. Key code for configuring the file Path: public class Path <TModel> {private TModel _ model; public Path (TModel model) {_ model = model ;} public PropertyItem <TValue> Property <TValue> (Expression <Func <TModel, TValue> propertyExpression) {var item = new PropertyItem <TValue> (propertyExpression. propertyName (), propertyExpression. propertyValue (_ model), _ model); return item ;}} to implement fluent, we also need to define a static non-generic class, public static class Pa Th {public static Path <TModel> For <TModel> (TModel) {var path = new Path <TModel> (model); return path ;}} defines Validator, this class can read the configuration information, public Validator <TValue> MapPath (params PropertyItem <TValue> [] properties) {foreach (var propertyItem in properties) {_ items. add (propertyItem);} return this;} Finally, call [Test] public void Should_validate_model_values () {// Arrange var validator = new Validator <st Ring> (); validator. mapPath (Path. for (_ student ). property (x => x. name), Path. for (_ student ). property (x => x. email), Path. for (_ customer ). property (x => x. name), Path. for (_ customer ). property (x => x. email), Path. for (_ manager ). property (x => x. name), Path. for (_ manager ). property (x => x. email )). onCondition (model) =>! String. isNullOrEmpty (model. toString (); // Act validator. validate (); // Assert var result = validator. result (); result. count. shocould (). be (3); result. any (x => x. modelType = typeof (Student) & x. name = "Email "). shocould (). be (true); result. any (x => x. modelType = typeof (Customer) & x. name = "Name "). shocould (). be (true); result. any (x => x. modelType = typeof (Manager) & amp; x. name = "Email "). shocould (). be (true );}