|
| _3u10 wrote:
| This feels like inheritance to me, AdminUser inherits from User
| both have a setup / create method... Or just use a constructor...
|
| For the most part I feel whatever gets the job done in the least
| lines of code is usually the right solution.
|
| But overall this entire exercise smells suspiciously like a
| constructor.
| code_runner wrote:
| Yeah I assumed we were going to get to some sort of
| types/inheritance based solution as well.
|
| Stuff like this doesn't bother me a ton unless it's littered
| everywhere. If every function can run one of two paths that's
| certainly annoying... if there is some high level function that
| builds one of two "pipelines" and I don't think about userType
| again, I don't care too much.
| yongjik wrote:
| The original version isn't a code smell. It has a very important
| advantage: read the function and you know that a user can be only
| one of the two things, a "customer" or an "admin", and you never
| have to worry about any other cases, because they don't exist.
|
| Make it a callback, and now the code becomes "Here we do
| something specific to each subclass of users, but this code
| doesn't tell you what they are, there can be any number of
| classes and the callback can literally do _anything_ , and if you
| want to figure out what's happening at this particular step,
| scour the entire codebase and find all the callbacks that are
| passed as arguments."
|
| The code has become extensible, and thus harder to reason about.
| This is a good trade-off if you _know_ you will have more and
| more user roles, added by different devs (or even different
| teams), and you want the user creation code to be decoupled from
| roles.
|
| On the other hand, if most of your business logic is "Do this for
| customers" and "Do that for admins," then it's a useless
| extensibility. You will need a major change of your business
| requirement before a callback would be useful.
|
| YAGNI.
| rcme wrote:
| "Do this for customers" and "do this for admins" has to be the
| number one regret of people who implement systems requiring
| access controls. Using roles and privileges is a much better
| approach.
| Octokiddie wrote:
| Some are suggesting that the author is against the use of tagged
| unions.
|
| I don't read it that way. What the author is against is using
| parameters for the specific purpose of controlling the flow of a
| function. Instead, the author recommends breaking the function up
| into as many functions as there are variants of the type key.
|
| This also applies to boolean flags (where only two variants are
| possible). There are, of course, exceptions as the author points
| out.
|
| The small win is that the type key no longer needs to be
| maintained. The big win is that each function that gets split out
| has fewer reasons to change. The individual functions are
| therefore easier to maintain and understand.
| dragonwriter wrote:
| > Some are suggesting that the author is against the use of
| tagged unions.
|
| > I don't read it that way. What the author is against is using
| parameters for the specific purpose of controlling the flow of
| a function. Instead, the author recommends breaking the
| function up into as many functions as there are variants of the
| type key.
|
| Sure. How do you call the right one of the combined set of
| functions? A conditional branching on the type key at the call
| site? But then, by the same rule, the _calling_ function should
| be split into multiple functions instead, and that recursisvely
| happens until you reach each point an instance of the typed
| union that _might_ , ever, be passed (indirectly) to the
| function at issue is defined. And then you have a bunch of sets
| of near-identical functions where every function in the set
| needs updated for any required change that _doesn't_ depend on
| the tagged union. And the tagged union is no longer really
| serving the purpose of a tagged union, which is exactly to
| allow _not_ having that kind of duplication.
| Fellshard wrote:
| I think the idea is that it's better to maintain several
| functions whose implementations are each linear flow than to
| maintain a single function that is littered with type key
| checks to turn on and off parts of its flow. It's trading off
| comprehension of each function by adding some risk that those
| functions will have shared parts that go out of sync.
| [deleted]
| motogpjimbo wrote:
| > The big win is that each function that gets split out has
| fewer reasons to change. The individual functions are therefore
| easier to maintain and understand.
|
| Yet the namespace becomes more polluted and the
| interconnections between the functions becomes more
| complicated, particularly during debugging.
|
| It's important to remember that "break big things into smaller
| things" only pushes complexity someone else rather than
| eliminating it altogether, while simultaneously increasing the
| overall complexity of the system by increasing the number of
| interconnections between the parts. That doesn't mean you
| shouldn't do it but you should be honest about the
| consequences.
| thdc wrote:
| If possible, I would prefer deriving the "userType" from
| UserAttributes.
|
| I see various methods for doing so are covered in the follow-up
| post linked at the top of tfa.
|
| This is also avoidable if you have proper class/interface
| hierarchies, where the type "field/key" is the type itself.
|
| In the case it's not possible and there really is only 1 type
| that is fully applicable to both admins and normal users then I
| would probably be fine with either approach (although I'd
| consider updating the return type to get type safety between
| admin and normal users!)
| dfee wrote:
| The author is against tagged unions. Ok.
|
| https://en.m.wikipedia.org/wiki/Tagged_union
| jjice wrote:
| The article felt like it was more against them being used in
| functions as switches. I don't think (could have missed it)
| that there was any discussion about using them in transport,
| because they are necessary there, but deserializing them to
| richer objects before going into code like was detailed in the
| article is a good idea IMO.
| Jtsummers wrote:
| The article actually specifies that they are known
| statically, and not actually dynamic values. They are _not_
| coming from databases or other external sources and that 's a
| large part of why it's a code smell. From the article, near
| the top but below where many people in this thread stopped
| reading:
|
| > its value is always known at compile time. We're not
| receiving it as an parameter from an HTTP request or a value
| from the database.
| fovc wrote:
| A common variant of this is the Boolean argument to flip between
| function modes doAOrB(args, aOrB: bool)
|
| Especially if the toggle gets used in 3-4 places or more in the
| body, control flow is a nightmare to follow. Instead of 2 paths
| to read, you have to mentally coalesce 2^n paths into 2. Worst-
| case scenario is when args has different meanings depending on
| the value of aOrB. @param flag: if set and if
| aOrB is true, warnings log to stdout and die, otherwise warnings
| are saved in the foobar field of args
| meindnoch wrote:
| My favorite version is the "forceX" antipattern, where you
| introduce boolean flags to turn off some aspect of a function.
| E.g. "loadData(forceFromDisk: bool)", where "forceFromDisk ==
| true" bypasses the cache.
|
| Then somewhere down the line someone realizes that actually in
| some cases of "forceFromDisk == true", we _still_ want to load
| the data from the cache, so the function becomes
| "loadData(forceFromDisk: bool, forceCache: bool)" where
| "forceCache == true" overrides "forceFromDisk".
|
| Then somewhere down the line someone realizes that actually in
| some cases of "forceCache == true", we still want to load from
| the disk... Repeat ad infinitum.
| gherkinnn wrote:
| Two or more coupled bools are crying to become a union. A
| function who's control flow needs to be configured cries to
| be split up.
| [deleted]
| jameshart wrote:
| My golden rules of programming:
|
| * All booleans really want to be enums
|
| * All enums really want to be rich data objects
|
| * All rich data objects really want to be discriminated unions
|
| That is, whenever you write some code which accepts a Boolean
| parameter that drives its behavior, at some point you will
| regret making it a bool and inevitably need to refactor it to
| be an enumeration type with more than two values. But then
| eventually you will realize that you have one behavior in your
| code that applies to more than one of those enum values (all
| the 'type a' cases but not the 'type b' cases), and you will
| want those enum values to themselves have a Boolean property
| telling you whether they are of type a or type b (and note that
| that Boolean will also be subject to this same golden rule in
| time)
|
| Eventually you'll find that those different enum values (the
| type a and type b ones) need different sets of dependent data
| (type a values all have a 'target' as well, but type b ones
| don't, say) - so you end up needing a discriminated union to
| capture the various data objects involved.
|
| All of which leads us to: every if statement and switch
| statement (over a parameter or input value) is a disguised
| match on a discriminated union type. Over time, this fate is
| inevitable.
|
| And if your language doesn't have discriminated unions, learn a
| pattern to fake them (usually it's the abstract factory
| pattern).
| jameshart wrote:
| Also: forgot the evolution into its final form, which is a
| list of rule objects.
|
| A practical example:
|
| Your program starts out accepting a config file that looks
| like this: enableLogging: true
|
| Then it changes into: logLevel: WARN
|
| Then that becomes: logging: {
| level: WARN file: system.log }
|
| Then: logging: fileLogger: {
| level: WARN file: system.log }
|
| And finally: logging: -
| fileLogger: { level: WARN
| file: system.log } - consoleLogger: {
| level: DEBUG }
| i_am_toaster wrote:
| The truth of this comment leaves me feeling somewhat
| emotionally violated.
| theteapot wrote:
| Question is whether and to what extent you can shortcut that
| lifecycle, and whether it's worth trying.
| jameshart wrote:
| Right. I tend to think you should just be aware of it, be
| deliberate in choosing where to situate your code on it,
| and be adept at refactoring code up the scale.
| d3nj4l wrote:
| I've seen the general idea here called "part blindness"
| before: https://www.hxa.name/notes/note-
| hxa7241-20131124T0927Z.html
| toolslive wrote:
| It's just the second law of Thermodynamics at work, no ?
| culi wrote:
| I think this evolution paradigm is useful to keep in mind
| when designing the API, but obviously each successive step is
| more computationally expensive and more complex (see: error-
| prone)
|
| You should probably generally use the least advanced of these
| while maintaining a pathway for future refactors into more
| complex versions of this
| player2121 wrote:
| This code is easy to read and understand and refactoring it with
| a callback seems as premature optimization which does bring own
| flavor to it. In other words, this is a good example when perfect
| is the enemy of the good.
| cbrogrammer wrote:
| > const createUser = (
|
| > attributes: UserAttributes,
|
| > onCreate: (user: User) => void
|
| > ): User => {
|
| > user = User.create(attributes);
|
| > onCreate(user);
|
| This stood out to me, in an otherwise tame article, as probably
| the most terrible overthought interface possible. A literal
| callback function passed as a function reference to something
| that does three lines of code? Good luck with that, I much prefer
| even a slightly procedual implementation.
| rglullis wrote:
| The three line function here is an example, but I spent a good
| part of a sprint having to deal with a 730-line function that
| could've been used as a perfect "real world scenario" of why
| this abuse of control flags (or type keys) is a bad idea.
| IshKebab wrote:
| Maybe, but the final solution is definitely harder to follow
| because the function definition now doesn't contain all the
| possible code that can run. You have to "find all references" and
| read the code of all the callers to know what it might do.
|
| It's more loosely coupled and that always makes code harder to
| follow. So I'd say it could go either way. Depends on the
| specific example.
| gpjanik wrote:
| This entire article assumes that the type is only used to satisfy
| what's on display in the example function, which is almost never
| true.
|
| The article itself is some sort of a programming flex I'd fire
| you for if you leave it in the code reviews too often. :P
| blowski wrote:
| I really dislike the trend for saying "I'd fire you for x"
| where x is merely a suboptimal choice, or even a harmless
| decision about which the author has strong opinions. Probably
| it is said in jest but it creates a hostile atmosphere for
| juniors, where they are worried they can be fired for using the
| wrong design pattern.
| sneak wrote:
| I envy your access to your recruiting organization.
| nonethewiser wrote:
| This has a name. It's called "control coupling." Like the
| author's example, passing in a flag to a function that controls
| what the function does is control coupling and you should prefer
| different functions for each case instead.
| klyrs wrote:
| > you should prefer different functions for each case instead.
|
| No I shouldn't. I use this frequently in c++ to deduplicate
| common functionality that only differs by a single line. The
| "flag" is a zero-size struct and checking the flag is done at
| compile time. struct do_bar_tag {}
| template int foo(int x, do_bar_t) {
| if constexpr (is_same::value) {
| bar(x); } ... }
|
| The compiler will emit two implementations, as you suggest is
| proper, but my maintenance overhead is reduced. And the cost is
| zero, in time and space, at runtime.
|
| Of course, this isn't _always_ the right thing to do, and you
| can definitely overdo it. But in moderation, it 's fine. "Code
| smell" is too often a needlessly strong opinion.
| tacotacotaco wrote:
| Just because you do it frequently doesn't necessarily make it
| good. Your approach increases the function's cyclomatic
| complexity. The struct requires the caller to dictate the
| function's inner logic instead of the function logic being
| driven by application data. Maybe your solution is a good fit
| in your production code. I can't say. Maybe there is a
| different approach to solve your boilerplate concerns. There
| will be exceptions but generally I don't agree with the
| example as provided.
| snarfy wrote:
| It never stays that way. It starts as
| foo(int x, do_bar_t)
|
| With time, it turns into foo(int x, int y,
| do_bar_t)
|
| and eventually foo(int x, int y, int z, int
| w, do_bar_t)
|
| It's too tempting to add 'just one more' flag. You de-
| duplicate common functionality but increase overall
| complexity.
| mycall wrote:
| Put the flags into a class and pass the class around.
| 0xCMP wrote:
| s/class/typed object/ -> "How to write Typescript"
| gpderetta wrote:
| And the issue with too many behavioural flags is that now
| the function (and the implementor) has to deal to all
| possible combinations, even though by specs only a small
| subset would be allowed.
| klyrs wrote:
| Only, I don't use ints as "flags," so you're doing it
| wrong. And you're using the slippery slope fallacy,
| assuming that I'm a DRY absolutist. I'm not. Like I said,
| it's not _always_ a good thing, and you can definitely
| overdo it.
|
| The valid complaint about the implementation I show would
| be that the do_bar logic is at the very top with no
| preamble, and it may be simpler for the caller to simply
| call bar(x) in that case. But adding necessary-looking
| complexity would detract from the clarity of the
| implementation (and damn c++'s verbosity, it already looks
| like hot garbage IMHO)
| kwhitefoot wrote:
| > Only, I don't use ints as "flags," so you're doing it
| wrong. And you're using the slippery slope fallacy,
| assuming that I'm a DRY absolutist. I'm not.
|
| You might not "use ints as flags" but a lot of
| maintenance programmers who know nothing about your idea,
| have never seen the code before, don't know how the
| application works or even what it does, will do just
| that.
| klyrs wrote:
| What you're describing is multiple policy failures. Why
| are these "maintenance programmers" implementing novel
| features? Why are they novices that are unfamiliar with
| the language and its best practices? Why are these
| novice-rogue-developer-maintainers' contributions not
| being reviewed by more competent developers?
|
| Moreover, if these unrestricted-novice-rogue-developer-
| maintainer-rhinoceri are going to do whatever the hell
| they feel like, completely disregarding the patterns and
| practices that I have established in my code, _does it
| matter what I have done?_
| xupybd wrote:
| Because novice programmers are cheaper and businesses
| make decisions based on short term cost. Especially when
| projects get old.
| klyrs wrote:
| I know all that. But you didn't address my point: if
| management has thrown all good practices to the wind, how
| am _I_ to blame for the horrendous outcomes?
| nomel wrote:
| This is a tangent, but I've never seen the real world
| work this way. You're being paid to be the DRI for the
| code base. You _will_ be blamed because you're being paid
| for it to be your responsibility. Actually horrendous
| outcomes, which can only be related to breaking
| functionality rather than code prettiness, can be
| prevented.
| [deleted]
| xupybd wrote:
| I'm not blaming you I'm saying most of the time
| management will pick the cheapest option. That leads to
| bad outcomes for a lot of legacy software. That then
| leads to someone spending more money replacing the legacy
| system
| mixedCase wrote:
| Part of the job is to know when to call it quits and
| refactor, there's no hard rule. In most cases I'll take a
| 130 line procedure with three flags that don't heavily
| interact with each other over three almost-identical 100
| line functions.
| culi wrote:
| Porque No los Dos?
|
| In languages with good support for functional features,
| you can curry and compose functions to your heart's
| content, allowing you to reuse similar logic and take
| better advantage of type systems like TypeScript's
| valand wrote:
| It comes down to how deep the type system of a language
| support this kind of operation. It'll work well with TS,
| Rust, but it doesn't add much to golang, C++.
|
| But well, usually dynamicizing statically-known types
| yields premature abstraction
| culi wrote:
| I would actually call it "atomization" more than
| abstraction, but I agree it can often be premature
| semicolon_storm wrote:
| For most modern programming problems that we get paid to
| solve, a bit of duplicate or similar looking code is the
| least of our problems. Conceptually integrity and managing
| dependencies/coupling is far more important.
| klyrs wrote:
| Who is "we" and do you have statistics behind that "most?"
| And why is "paid to solve" relevant to the discussion? It's
| okay to say that what I'm saying about my personal
| experience doesn't resonate with your personal experience,
| but hand-waving at a nebulous authority isn't making a
| case.
|
| I use compile-time flags to deduplicate logic because
| refactoring is a nightmare if tens to hundreds of lines of
| code are turned into boilerplate. That is to say, I ensure
| conceptual integrity by maintaining a single source of
| truth. The nice thing about relatively dry code is that if
| you need to decouple it, you can easily just duplicate the
| code in question and run with that. If you have a dozen
| slightly-different copies of the same boilerplate, it's
| very rare in my experience for the variations to be well
| documented, so it ends up taking a lot of work to figure
| out why the variations exist -- and the real pitfall that
| I've seen time and time again is that a bug is found in one
| variant, fixed there, and not propagated to the other
| variants.
| rcarr wrote:
| > Who is "we" and do you have statistics behind that
| "most?"
|
| I love that you've called this out. So many people try
| and justify their coding preferences by referring to this
| mythical 'we' or the 'coding community' who have
| apparently all agreed to to some certain standard and
| everyone should just fall in line and get behind it
| without asking any questions. It's such a load of
| bullshit.
| valand wrote:
| When I shared this to my colleague, I call it "dynamicizing
| statically-known info". It is in the same family as putting
| hardcoded values in a list and calling a .map to it. It almost
| always end as a premature abstraction.
| aliqot wrote:
| > This has a name.
|
| Good, I'm glad. For some reason the term "Code smell" has
| itself become a "smell" to me.
| [deleted]
| thecodrr wrote:
| Not everything needs to be DRY. In a lot of codebases, I see this
| hell-bent attitude to make everything as DRY as possible often at
| the cost of creating multiple files when all it'd require is 2
| functions.
|
| A lot of code smells arise from this attitude. DRY is not
| mandatory. Oftentimes, repeating yourself is better and clearer.
| It allows you to grow both functions at their own pace, handle
| edge cases separately, prevent spaghetti code, write better &
| simpler tests.
| sjducb wrote:
| I 100% agree, but try telling that to a programmer with 2 years
| experience.
| wizofaus wrote:
| Even with over 25 years professional experience I still see
| code where logic and literal constant values (including e.g.
| complex regular expressions) are duplicated unnecessarily
| (requiring constant double maintenance etc.) as more of a
| problem than overly clever ways to reduce repetition that
| make the code harder to work with. 95% of the time developers
| can easily save themselves time upfront and in the future by
| first checking if the logic they're writing already exists in
| the codebase somewhere and reusing that (usually with very
| minor refactoring, e.g. marking a function public and moving
| it into a suitable shared module). Maybe 1% of the time the
| necessary refactoring is complex enough that it risks
| introducing bugs or decreasing code comprehensibility and
| duplicating the logic (ideally with a comment referring to
| the original source) is the lesser evil.
| deathanatos wrote:
| > _It allows you to grow both functions at their own pace,
| handle edge cases separately,_
|
| DRY should only be applied to semantically identical code, not
| merely syntactically identical code. If the code is in the
| former category, and should be subject to DRY, and you're
| making changes to one copy, by definition you must need to make
| changes to the other copy ... and it's a bug if you're not.
|
| And those are the _worse* kinds of repetition to encounter
| later on when you 're trying to change the code: I've got two
| functions, ostensibly doing the same thing ... except not.
| Which one is correct? (I.e., "What are the requirements?", and
| in my career, if I'm encountering this situation in the code,
| the requirements are _never* documented.)
| rhdunn wrote:
| Type keys are used in various serialization mechianisms,
| especially JSON/TypeScript APIs (JSON-RPC, RDF-JSON, etc.). Even
| XML serializations have type keys with the element names.
|
| In these cases, it makes sense to hide those type keys from the
| API and only use them in the serialization/deserialization logic.
| kstenerud wrote:
| The problem with these examples is that they're contrived and
| oversimplified. Yes, switched enum behavior can be bad, but it
| can also be the right solution for the time being (or even
| forever).
|
| The solution in the article could turn out even worse if the
| system evolves to require so many kinds of users that you end up
| with 8 different functions depending on what kind of user you
| want. You can't hide complexity of this type; you can only move
| it around as it emerges and try to keep things understandable.
|
| In every design situation you're faced with countless approaches
| that you can take, with a lot of uncertainty as to how the system
| will evolve over time. I've found that the "what if" time you
| spend up front follows a parabolic efficiency curve: Early on in
| the design, you throw away some ideas that would make the first
| implementation easy, but would quickly become unwieldly. As the
| design progresses, you start thinking of more and more "what
| ifs", and soon you find your up-front architecture getting
| heavier and heavier, such that if you don't stop with the "what
| ifs", your first implementation will become a cathedral when all
| you'll need for the next year is a priest and a bench.
|
| Clear out some easy win design issues up front, but overall don't
| sweat it and don't spend much time thinking "what if" because
| you're going to predict the future 50% wrong anyway. Refactoring
| isn't that hard or time intensive except in old code bases with
| poor hygene, and your new project is neither (unless you have
| poor discipline).
| martin-adams wrote:
| As a nice side effect of fixing this is that it removed the bug
| where the switch statement didn't have a break statement.
| qayxc wrote:
| To be fair, every reasonable linter will treat this as an error
| or at least a warning and inform the programmer about their
| blunder. In fact, in TypeScript you don't even need a linter -
| the compiler itself can be configured to disallow fall-through
| cases. It's smart about it, too and still allows "empty" cases
| and makes the last case-statement optional, such that "switch
| (lorem) { case 'foo': case 'bar': ipsum(); break; case '...':
| blah(); } is still valid and compiles just fine even with
| noFallthroughCases configured.
| Nition wrote:
| > Note that instead of using callbacks we could have subclassed
| User to AdminUser and RegularUser, with each overriding the
| onCreate function.
|
| Although the pendulum has swung far from inheritance towards
| composition in recent years, this is surely a prime case for
| simply using a subclass and instantiating whichever one you want.
| rcarr wrote:
| I'm not sure I agree with this. Surely the solution is not to
| bother with the completely pointless createAdmin and
| createCustomer functions in the first place and to just call
| createUser directly where it is needed with the correct
| parameter. Otherwise you're just repeating yourself.
|
| Most of the time, DRY is better.
| auraham wrote:
| When I started reading the post, I though the author would talk
| about pattern matching to get rid of the switch block. In that
| case, creating a user can be expressed in Elixir as follows:
| def createUser(attributes, "admin") do
| create(attributes) |> setupAdmin |> setupNotifications
| end def createUser(attributes, "customer") do
| create(attributes) |> setupCustomer |> setupNotifications
| end # example User.createUser(%{name:
| "laura", age: 30}, "admin")
|
| Here, we chain functions using a pipe, |>, assuming that each
| function returns a user-like variable, like %User{}
|
| I also liked the idea of using a callback for decoupling code
| (although that approach was discouraged by many users in this
| post). It could be done in Elixir as follows:
| def createUserCallback(attributes, callback) do
| create(attributes) |> callback.() |> setupNotifications
| end # example
| User.createUserCallback(%{name: "laura", age: 30},
| &User.setupCustomer/1) User.createUserCallback(%{name:
| "laura", age: 30}, &User.setupAdmin/1)
|
| Since callback can be any kind of function, we can use pattern
| matching to enforce that the return type of each function is a
| user, ie %User{}: def
| createUserCallback(attributes, callback) do user =
| %User{} = create(attributes) user = %User{} =
| callback.(user) user = %User{} =
| setupNotifications(user) user end
|
| Another approach is using a @spec annotation to define the
| signature of the callback.
|
| Full code: defmodule User do
| defstruct name: nil, age: nil, type: nil defp
| create(_attributes = %{name: name, age: age}) do
| %User{name: name, age: age} end defp
| setupNotifications(user = %User{}) do IO.puts("User
| created #{user.type}: #{user.name}") user
| end def setupAdmin(user = %User{}) do
| %User{ user | type: "admin" } end def
| setupCustomer(user = %User{}) do %User{ user | type:
| "customer" } end def
| createUser(attributes, "admin") do create(attributes)
| |> setupAdmin |> setupNotifications end
| def createUser(attributes, "customer") do
| create(attributes) |> setupCustomer |> setupNotifications
| end def createUserCallback(attributes, callback)
| do user = %User{} = create(attributes)
| user = %User{} = callback.(user) user = %User{} =
| setupNotifications(user) user end
| end
| swisniewski wrote:
| The author has probably never written a compiler before.
|
| Inside any compiler, you are going to have "type keys" and switch
| statements all over the place.
|
| Even if you are writing a compiler in languages with builtin
| "tagged unions" and "pattern matching" you are still using "type
| keys" and switch statements.
|
| Trying to write a compiler without a switch statement would lead
| to a giant unreadable mess.
| tincholio wrote:
| Sounds like he could use a language with multimethods...
| SecondRikudo wrote:
| I mean... you already have user.setupNotifications() in your
| example... the obvious solution would have been a polymorphic
| user.setup() which is a different implementation for the
| CustomerUser class and the AdminUser class... if you've already
| picked OO as your paradigm of choice, at least exploit it for its
| value, you've already paid the costs.
| pfraze wrote:
| These kinds of edicts don't work because they fail the most
| important rule of programming: do what's simplest and most clear
| for the code you're writing. Your language is going to have
| constructs for this kind of thing (polymorphic inheritance,
| discriminated unions, multi dispatch, etc). Pick the language-
| native construct that works best. In the case of Typescript this
| would arguably be polymorphic inheritance, but if you don't have
| a scenario that benefits in multiple ways from writing classes
| then using them would be overkill.
| rosebay wrote:
| I read the blog, was mortified but luckily others have already
| provided feedback in an earlier thread:
| https://news.ycombinator.com/item?id=28325563
|
| To the author's credit, I loved that he is thinking analytically
| and he did take the feedback positively - there are some good
| ideas in the post.
| svnpenn wrote:
| The code smell I see is the omitted semicolons. The pitfalls of
| doing this are well documented. Just don't do it.
| gherkinnn wrote:
| The commonly cited pitfalls are either strawmen or exceedingly
| rare. ASI is predictable and reliable. Not once in 5 years of
| semicolon-less development did it come to bite me or anybody
| else in a notable manner.
|
| By all means use them in your work, but don't treat it as
| ultimate truth. It is not.
| exelib wrote:
| Not sure I agree with the author. It took me less than 3 seconds
| to understand the initial code, but at least 10 seconds to
| understand the final result, even I had the knowledge of the
| initial function. But in some context this pattern can make a
| perfectly sense. Just not in this example.
|
| Furthermore, from my point of view, creating an anonymous
| function and then store it in a variable is a bigger code smell.
| satvikpendem wrote:
| In a language like Rust or OCaml with exhaustive pattern matching
| on algebraic data types, the initial example is the recommended
| approach, because you will never miss a case, it forces you to
| think through all possible cases, where as the later refactored
| example in the article do not have such a constraint.
| qayxc wrote:
| The same is true for Typescript, hence the userType's
| restriction to 'customer' and 'admin'.
|
| Though the initial sample code is wrong anyway - note the
| missing "break" statements inside the switch.
| metadat wrote:
| Previously discussed in August 2021:
|
| https://news.ycombinator.com/item?id=28325563 (74 comments)
|
| Credit: Thanks to @rosebay for pointing this out at
| https://news.ycombinator.com/item?id=34218449 (now dead)
|
| Also see additional interesting submissions from
| jesseduffield.com:
|
| https://news.ycombinator.com/from?site=jesseduffield.com
___________________________________________________________________
(page generated 2023-01-02 23:01 UTC) |