Method chaining - why is it a good practice, or not?

限于喜欢 提交于 2019-12-27 10:35:30

问题


Method chaining is the practice of object methods returning the object itself in order for the result to be called for another method. Like this:

participant.addSchedule(events[1]).addSchedule(events[2]).setStatus('attending').save()

This seems to be considered a good practice, since it produces readable code, or a "fluent interface". However, to me it instead seems to break the object calling notation implied by the object orientation itself - the resulting code does not represent performing actions to the result of a previous method, which is how object oriented code is generally expected to work:

participant.getSchedule('monday').saveTo('monnday.file')

This difference manages to create two different meanings for the dot-notation of "calling the resulting object": In the context of chaining, the above example would read as saving the participant object, even though the example is in fact intended to save the schedule object received by getSchedule.

I understand that the difference here is whether the called method should be expected to return something or not (in which case it would return the called object itself for chaining). But these two cases are not distinguishable from the notation itself, only from the semantics of the methods being called. When method chaining is not used, I can always know that a method call operates on something related to the result of the previous call - with chaining, this assumption breaks, and I have to semantically process the whole chain to understand what the actual object being called really is. For example:

participant.attend(event).setNotifications('silent').getSocialStream('twitter').postStatus('Joining '+event.name).follow(event.getSocialId('twitter'))

There the last two method calls refer to the result of getSocialStream, whereas the ones before refer to the participant. Maybe it's bad practice to actually write chains where the context changes (is it?), but even then you'll have to constantly check whether dot-chains that look similar are in fact keep within the same context, or only work on the result.

To me it appears that while method chaining superficially does produce readable code, overloading the meaning of the dot-notation only results in more confusion. As I don't consider myself a programming guru, I assume the fault is mine. So: What am I missing? Do I understand method chaining somehow wrong? Are there some cases where method chaining is especially good, or some where it's especially bad?

Sidenote: I understand this question could be read as a statement of opinion masked as a question. It, however, isn't - I genuinely want to understand why chaining is considered good practice, and where do I go wrong in thinking it breaks the inherent object-oriented notation.


回答1:


I agree that this is subjective. For the most part I avoid method chaining, but recently I also found a case where it was just the right thing - I had a method which accepted something like 10 parameters, and needed more, but for the most time you only had to specify a few. With overrides this became very cumbersome very fast. Instead I opted for the chaining approach:

MyObject.Start()
    .SpecifySomeParameter(asdasd)
    .SpecifySomeOtherParameter(asdasd)
    .Execute();

This is something like a factory pattern. The method chaining approach was optional, but it made writing code easier (especially with IntelliSense). Mind you that this is one isolated case though, and is not a general practice in my code.

The point is - in 99% cases you can probably do just as well or even better without method chaining. But there is the 1% where this is the best approach.




回答2:


Just my 2 cents;

Method chaining makes debugging tricky: - You can't put the breakpoint in a concise point so you can pause the program exactly where you want it - If one of these methods throws an exception, and you get a line number, you have no idea which method in the "chain" caused the problem.

I think it's generally good practice to always write very short and concise lines. Every line should just make one method call. Prefer more lines to longer lines.

EDIT: comment mentions that method chaining and line-breaking are separate. That is true. Depending on the debugger though, it may or may not be possible to place a break point in the middle of a statement. Even if you can, using separate lines with intermediate variables gives you a lot more flexibility and a whole bunch of values you can examine in the Watch window that helps the debugging process.




回答3:


Personally, I prefer chaining methods that only act on the original object, e.g. setting multiple properties or calling utility-type methods.

foo.setHeight(100).setWidth(50).setColor('#ffffff');
foo.moveTo(100,100).highlight();

I do not use it when one or more of the chained methods would return any object other than foo in my example. While syntactically you can chain anything as long as you are using the correct API for that object in the chain, changing objects IMHO makes things less readable and can be really confusing if the APIs for the different objects have any similarities. If you do some really common method call at the end (.toString(), .print(), whatever) which object are you ultimately acting upon? Someone casually reading the code might not catch that it would be an implicitly returned object in the chain rather than the original reference.

Chaining different objects can also lead to unexpected null errors. In my examples, assuming that foo is valid, all the method calls are "safe" (e.g., valid for foo). In the OP's example:

participant.getSchedule('monday').saveTo('monnday.file')

...there's no guarantee (as an outside developer looking at the code) that getSchedule will actually return a valid, non-null schedule object. Also, debugging this style of code is often a lot harder since many IDEs will not evaluate the method call at debug time as an object that you can inspect. IMO, anytime you might need an object to inspect for debugging purposes, I prefer to have it in an explicit variable.




回答4:


Martin Fowler has a good discussion here:

Method Chaining

When to use it

Method Chaining can add a great deal to the readability of an internal DSL and as a result has become almost a synonum for internal DSLs in some minds. Method Chaining is best, however, when it's used in conjunction with other function combinations.

Method Chaining is particularly effective with grammars like parent::= (this | that)*. The use of different methods provides readable way of seeing which argument is coming next. Similarly optional arguments can be easily skipped over with Method Chaining. A list of mandatory clauses, such as parent::= first second doesn't work so well with the basic form, although it can be supported well by using progressive interfaces. Most of the time I'd prefer Nested Function for that case.

The biggest problem for Method Chaining is the finishing problem. While there are workarounds, usually if you run into this you're better off usng a Nested Function. Nested Function is also a better choice if you are getting into a mess with Context Variables.




回答5:


In my opinion, method chaining is a bit of a novelty. Sure, it looks cool but I don't see any real advantages in it.

How is:

someList.addObject("str1").addObject("str2").addObject("str3")

any better than:

someList.addObject("str1")
someList.addObject("str2")
someList.addObject("str3")

The exception might be when addObject() returns a new object, in which case the unchained code may be a little more cumbersome like:

someList = someList.addObject("str1")
someList = someList.addObject("str2")
someList = someList.addObject("str3")



回答6:


It is dangerous because you may be depending on more objects than expected, like then your call returns an instance of another class:

I will give an example:

foodStore is an object that is composed of many food stores you own. foodstore.getLocalStore() returns an object that holds information on the closest store to the parameter. getPriceforProduct(anything) is a method of that object.

So when you call foodStore.getLocalStore(parameters).getPriceforProduct(anything)

you are depending not only on FoodStore as you though, but also on LocalStore.

Should getPriceforProduct(anything) ever changes, you need to change not only FoodStore but also the class that called the chained method.

You should always aim for loose coupling among classes.

That being said, i personally like to chain them when programming Ruby.




回答7:


This seems kinda subjective.

Method chaining is not soemthing that is inherently bad or good imo.

Readability is the most important thing.

(Also consider that having large numbers of methods chained will make things very fragile if something changes)




回答8:


Many use method chaining as a form of convenience rather than having any readability concerns in mind. Method chaining is acceptable if it involves performing the same action on the same object - but only if it actually enhances readability, and not just for writing less code.

Unfortunately many use method chaining as per the examples given in the question. While they can still be made readable, they are unfortunately causing high coupling between multiple classes, so it's not desirable.




回答9:


Benefits of Chaining
ie, where I like to use it

One benefit of chaining that I did not see mentioned was the ability to use it during variable initiation, or when passing a new object to a method, not sure if this is bad practice or not.

I know this is contrived example but say you have the following classes

Public Class Location
   Private _x As Integer = 15
   Private _y As Integer = 421513

   Public Function X() As Integer
      Return _x
   End Function
   Public Function X(ByVal value As Integer) As Location
      _x = value
      Return Me
   End Function

   Public Function Y() As Integer
      Return _y
   End Function
   Public Function Y(ByVal value As Integer) As Location
      _y = value
      Return Me
   End Function

   Public Overrides Function toString() As String
      Return String.Format("{0},{1}", _x, _y)
   End Function
End Class

Public Class HomeLocation
   Inherits Location

   Public Overrides Function toString() As String
      Return String.Format("Home Is at: {0},{1}", X(), Y())
   End Function
End Class

And say you don't have access to the base class, or Say the default values are dynamic, based on time, etc. Yes you could instantiate then, then change the values but that can become cumbersome, especially if you're just passing the values to a method:

  Dim loc As New HomeLocation()
  loc.X(1337)
  PrintLocation(loc)

But isn't this just much easier to read:

  PrintLocation(New HomeLocation().X(1337))

Or, what about a class member?

Public Class Dummy
   Private _locA As New Location()
   Public Sub New()
      _locA.X(1337)
   End Sub
End Class

vs

Public Class Dummy
   Private _locC As Location = New Location().X(1337)
End Class

This is how I've been using chaining, and typically my methods are just for configuration, so they are only 2 lines long, set a value, then Return Me. For us it has cleaned up huge lines very hard to read and understand code into one line that read like a sentence. something like

New Dealer.CarPicker().Subaru.WRX.SixSpeed.TurboCharged.BlueExterior.GrayInterior.Leather.HeatedSeats

Vs Something like

New Dealer.CarPicker(Dealer.CarPicker.Makes.Subaru
                   , Dealer.CarPicker.Models.WRX
                   , Dealer.CarPicker.Transmissions.SixSpeed
                   , Dealer.CarPicker.Engine.Options.TurboCharged
                   , Dealer.CarPicker.Exterior.Color.Blue
                   , Dealer.CarPicker.Interior.Color.Gray
                   , Dealer.CarPicker.Interior.Options.Leather
                   , Dealer.CarPicker.Interior.Seats.Heated)

Detriment Of Chaining
ie, where I don't like to use it

I don't use chaining when there are a lot of parameters to pass to the routines, mainly because the lines get very long, and as the OP mentioned it can get confusing when you're calling routines to other classes to pass to one of the chaining methods.

There is also the concern that a routine would return invalid data, thus so far I've only used chaining when I'm returning the same instance being called. As was pointed out if you chain between classes you make debuging harder (which one returned null?) and can increase dependencies coupling among classes.

Conclusion

Like everything in life, and programming, Chaining is neither good, nor bad if you can avoid the bad then chaining can be a great benefit.

I try to follow these rules.

  1. Try not to chain between classes
  2. Make routines specifically for chaining
  3. Do only ONE thing in a chaining routine
  4. Use it when it improves readability
  5. Use it when it makes code simpler



回答10:


Method chaining can allow for designing advanced DSLs in Java directly. In essence, you can model at least these types of DSL rules:

1. SINGLE-WORD
2. PARAMETERISED-WORD parameter
3. WORD1 [ OPTIONAL-WORD]
4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B }
5. WORD3 [ , WORD3 ... ]

These rules can be implemented using these interfaces

// Initial interface, entry point of the DSL
interface Start {
  End singleWord();
  End parameterisedWord(String parameter);
  Intermediate1 word1();
  Intermediate2 word2();
  Intermediate3 word3();
}

// Terminating interface, might also contain methods like execute();
interface End {}

// Intermediate DSL "step" extending the interface that is returned
// by optionalWord(), to make that method "optional"
interface Intermediate1 extends End {
  End optionalWord();
}

// Intermediate DSL "step" providing several choices (similar to Start)
interface Intermediate2 {
  End wordChoiceA();
  End wordChoiceB();
}

// Intermediate interface returning itself on word3(), in order to allow for
// repetitions. Repetitions can be ended any time because this interface
// extends End
interface Intermediate3 extends End {
  Intermediate3 word3();
}

With these simple rules, you can implement complex DSL's such as SQL directly in Java, as is done by jOOQ, a library that I have created. See a rather complex SQL example taken from my blog here:

create().select(
    r1.ROUTINE_NAME,
    r1.SPECIFIC_NAME,
    decode()
        .when(exists(create()
            .selectOne()
            .from(PARAMETERS)
            .where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA))
            .and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME))
            .and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))),
                val("void"))
        .otherwise(r1.DATA_TYPE).as("data_type"),
    r1.NUMERIC_PRECISION,
    r1.NUMERIC_SCALE,
    r1.TYPE_UDT_NAME,
    decode().when(
    exists(
        create().selectOne()
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))),
        create().select(count())
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField())
    .as("overload"))
.from(r1)
.where(r1.ROUTINE_SCHEMA.equal(getSchemaName()))
.orderBy(r1.ROUTINE_NAME.asc())
.fetch()

Another nice example is jRTF, a little DSL designed for cerating RTF documents directly in Java. An example:

rtf()
  .header(
    color( 0xff, 0, 0 ).at( 0 ),
    color( 0, 0xff, 0 ).at( 1 ),
    color( 0, 0, 0xff ).at( 2 ),
    font( "Calibri" ).at( 0 ) )
  .section(
        p( font( 1, "Second paragraph" ) ),
        p( color( 1, "green" ) )
  )
).out( out );



回答11:


Method chaining may simply be a novelty for most cases but I think it has it's place. One example might be found in CodeIgniter's Active Record use:

$this->db->select('something')->from('table')->where('id', $id);

That looks a lot cleaner (and makes more sense, in my opinion) than:

$this->db->select('something');
$this->db->from('table');
$this->db->where('id', $id);

It really is subjective; Everyone has their own opinion.




回答12:


I think the primary fallacy is thinking this is an object oriented approach in general when in fact it is more of a functional programming approach than anything else.

The main reasons I use it is for both readability and preventing my code from being inundated by variables.

I don't really understand what others are talking about when they say it damages readability. It is one of the most concise and cohesive form of programming I have used.

Also this:

convertTextToVoice.LoadText("source.txt").ConvertToVoice("destination.wav");

is how I would typically use it. Using it to chain x number of parameters is not how I typically use it. If I wanted to put in x number of parameters in a method call I would use the params syntax:

public void foo(params object[] items)

And cast the objects based on type or just use a datatype array or collection depending on your use case.




回答13:


I agree, I therefor changed the way an fluent interface was implemented in my library.

Before:

collection.orderBy("column").limit(10);

After:

collection = collection.orderBy("column").limit(10);

In the "before" implementation the functions modified the object and ended in return this. I changed the implementation to return a new object of the same type.

My reasoning for this change:

  1. The return value had nothing to do with the function, it was purely there to support the chaining part, It should have been a void function according to OOP.

  2. Method chaining in system libraries also implement it that way (like linq or string):

    myText = myText.trim().toUpperCase();
    
  3. The original object remains intact, allowing the API user to decide what to do with it. It allows for:

    page1 = collection.limit(10);
    page2 = collection.offset(10).limit(10);
    
  4. A copy implementation can also be used for building objects:

    painting = canvas.withBackground('white').withPenSize(10);
    

    Where the setBackground(color) function changes the instance and returns nothing (like its supposed to).

  5. The behavior of the functions are more predicable (See point 1 & 2).

  6. Using a short variable name can also reduce code-clutter, without forcing a api on the model.

    var p = participant; // create a reference
    p.addSchedule(events[1]);p.addSchedule(events[2]);p.setStatus('attending');p.save()
    

Conclusion:
In my opinion a fluent interface that uses an return this implementation is just wrong.




回答14:


The totally missed point here, is that method chaining allows for DRY. It's an effective stand-in for the "with" (which is poorly implemented in some languages).

A.method1().method2().method3(); // one A

A.method1();
A.method2();
A.method3(); // repeating A 3 times

This matters for the same reason DRY always matters; if A turns out to be an error, and these operations need to be performed on B, you only need to update in 1 place, not 3.

Pragmatically, the advantage is small in this instance. Still, a little less typing, a litle more robust (DRY), I'll take it.




回答15:


I generally hate method chaining because I think it worsens readability. Compactness is often confused with readability, but they are not the same terms. If you do everything in a single statement then that is compact, but it is less readable (harder to follow) most of the times than doing it in multiple statements. As you noticed unless you cannot guarantee that the return value of the used methods are the same, then method chaining will be a source of confusion.

1.)

participant
    .addSchedule(events[1])
    .addSchedule(events[2])
    .setStatus('attending')
    .save();

vs

participant.addSchedule(events[1]);
participant.addSchedule(events[2]);
participant.setStatus('attending');
participant.save()

2.)

participant
    .getSchedule('monday')
        .saveTo('monnday.file');

vs

mondaySchedule = participant.getSchedule('monday');
mondaySchedule.saveTo('monday.file');

3.)

participant
    .attend(event)
    .setNotifications('silent')
    .getSocialStream('twitter')
        .postStatus('Joining '+event.name)
        .follow(event.getSocialId('twitter'));

vs

participant.attend(event);
participant.setNotifications('silent')
twitter = participant.getSocialStream('twitter')
twitter.postStatus('Joining '+event.name)
twitter.follow(event.getSocialId('twitter'));

As you can see you win close to nothing, because you have to add line breaks to your single statement to make it more readable and you have to add indentation to make it clear that you are talking about different objects. Well if I'd want to use an identation based language, then I would learn Python instead of doing this, not to mention that most of the IDEs will remove the indentation by auto formatting the code.

I think the only place where this kind of chaining can be useful is piping streams in CLI or JOINing multiple queries together in SQL. Both have a price for multiple statements. But if you want to solve complex problems you will end up even with those paying the price and writing the code in multiple statements using variables or writing bash scripts and stored procedures or views.

As of the DRY interpretations: "Avoid the repetition of knowledge (not the repetition of text)." and "Type less, don't even repeat texts.", the first one what the principle really means, but the second one is common misunderstanding because many people cannot understand overcomplicated bullshit like "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system". The second one is compactness at all cost, which breaks in this scenario, because it worsens readability. The first interpretation breaks by DDD when you copy code between bounded contexts, because loose coupling is more important in that scenario.




回答16:


The good:

  1. It's terse, yet allows you to put more into a single line elegantly.
  2. You can sometimes avoid the use of a variable, which may occasionally be useful.
  3. It may perform better.

The bad:

  1. You're implementing returns, essentially adding functionality to methods on objects that isn't really a part of what those methods are meant to do. It's returning something you already have purely to save a few bytes.
  2. It hides context switches when one chain leads to another. You can get this with getters, except it's pretty clear when the context switches.
  3. Chaining over multiple lines looks ugly, doesn't play well with indentation and can cause some operator handling confusion (especially in languages with ASI).
  4. If you want to start returning something else that's useful for a chained method, you're potentially going to have a harder time fixing it or hit more problems with it.
  5. You're offloading control to an entity that you wouldn't normally offload to purely for convenient, even in strictly typed languages mistakes caused by this cannot always be detected.
  6. It may perform worse.

General:

A good approach is to not use chaining in general until situations arise or specific modules would be particularly suited to it.

Chaining can hurt readability quite severely in some cases especially when weighing in point 1 and 2.

On accasation it can be misused, such as instead of another approach (passing an array for example) or mixing methods in bizarre ways (parent.setSomething().getChild().setSomething().getParent().setSomething()).




回答17:


Opinionated Answer

The biggest drawback of chaining is that it can be hard for the reader to understand how each method affects the original object, if it does, and what type does every method return.

Some questions:

  • Do methods in the chain return a new object, or the same object mutated?
  • Do all methods in the chain return the same type?
  • If not, how is indicated when a type in the chain changes?
  • Can the value returned by the last method be safely discarded?

Debugging, in most languages, can indeed be harder with chaining. Even if each step in the chain is on its own line (which kind of defeats the purpose of chaining), it can be hard to inspect the value returned after each step, specially for non-mutating methods.

Compile times can be slower depending on the language and compiler, as expressions can be much more complex to resolve.

I believe that like with everything, chaining is a good solution that can be handy in some scenario. It should be used with caution, understanding the implications, and limiting the number of chain elements to a few.



来源:https://stackoverflow.com/questions/1103985/method-chaining-why-is-it-a-good-practice-or-not

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!