21 ms·
A Theory of Software Architecture
- kumarvvr 6y agoMove most of your code to pure functions, that is the number one mantra to solve most scaling problems in software development. Even in an OOP paradigm, it makes sense to make almost all objects as purely functional, with limited use of internal state. The second mantra is to think a thousand times before you name something. I actually keep a list of names (..Manager, ..aggregator,etc.) gleaned from various sources, to name my classes. Earlier, I used to name most of my classes as <Function/Operation>Manager Edit : Here is a question, that actually prompted me to keep a list. https://stackoverflow.com/questions/1866794/naming-classes-how-to-avoid-calling-everything-a-whatevermanager https://stackoverflow.com/questions/1866794/naming-classes-h...
- oaiey 6y agoKill "Manager" of that list :). Adds to the naming burden (aka quality) by removing the fallback :). I will adopt the list idea!!!
- Areading314 6y agoScaling is not the only way to accomplish high-performance. In many cases performance can be achieved with stateful, imperative yet efficiently executed code that economizes on the use of resources on a single node. In many cases, such as for mobile, desktop, or on-prem deployments, this is the only way to "scale" since you do not have the luxury of increasing number of nodes on demand.
- oaiey 6y agoPure functions are a good solution for a onion in a request / response pattern (which a lot of the scenarios are). However, an onion architecture representing something with state (e.g. a shadow Dom implementation), sometimes a OO inner core is the right solution.
- jonahx 6y ago> (..Manager, ..aggregator,etc.) Names ending in Manager are usually a poor choice. See Peter Coad's "-er-er" principle: > The “-er-er” principle. Challenge any class name that ends in “-er.” If it has no parts, change the name of the class to what each object is managing. If it has parts, put as much work in the parts that the parts know enough to do themselves. See also: http://www.carlopescio.com/2011/04/your-coding-conventions-are-hurting-you.html http://www.carlopescio.com/2011/04/your-coding-conventions-a...
- aryehof 6y agoI think the commenter generally views things as code acting on entities. If so, that code is suited to being called an xxxManager, or xxxService, or xxxCoordinator, or xxxController. Of course we have returned to a place in history by doing so, of creating big balls of mud as complexity increases. Peter Coad advocated against this in favor of modeling the problem domain under consideration using an object oriented approach. In an object oriented domain model there is no place for such external “controllers”, but I think the commenter doesn’t propose this approach.
- kumarvvr 6y ago>modeling the problem domain under consideration using an object oriented approach Really curious. Do you have any material that explains this way of design? I work on mostly web apps. End of the day, it's really about moving data and transforming data. So most of my programs have no choice but to deal with data, and so, my whole design process revolves around gathering, storing, operating upon and transferring data.
- cryptos 6y agoI don't know Peter Coad, but the approach reminds me of domain driven design (DDD). Most business logic would be in objects named entities, but DDD has also services, since there might be logic that affects multiple entities (actually special entities called "aggregate root").
- aryehof 6y ago
- forgotmypw17 6y agoAgree on the functional bit. Naming things: I try to not think about it for more than 10 seconds, and go with the best I've got by then. I find myself renaming things sometimes, and I'm eager to do this when a better name comes to me.
- simongray 6y agoI use the thesaurus that comes with macOS whenever I think of a name and it just doesn't feel right. Then I typically find a more fitting name after spending 20 seconds looking at near synonyms.
- ramraj07 6y agoOr hopefully your code reviewer suggests names that are better if your choice doesn't make sense
- forgotmypw17 6y agoThat is true. I am also the code reviewer at this time, but I do perform code reviews.
- jariel 6y agoYes this. Naming is an intuitive thing, you can't force it, and it will get in the way, moreover, it's fluid and won't matter until later, things could change. Just name it whatever and come back to it when it starts to matter more and you've probably thought of something better by then.
- philipov 6y agoRenaming things is a luxury only enjoyed by people who don't have other people using their code downstream. Once people have used it, renaming things becomes a breaking change others in your organization will oppose.
- kumarvvr 6y agoI did try that. That is how I ended up with 50 "Manager" classes in my app. At that point, it is a cognitive burden to handle so many "managers".
- hnxs 6y agoWould love to see your list of names if you’re willing to share!
- kumarvvr 6y agoI usually refer to this link. https://stackoverflow.com/questions/1866794/naming-classes-how-to-avoid-calling-everything-a-whatevermanager https://stackoverflow.com/questions/1866794/naming-classes-h...
- dheerajvs 6y agoAnother pet peeve of mine is variable names suffixed with "Data" and "Info".
- ahartmetz 6y agoYes, but... sometimes you have data about data (aka metadata) or about a thing, in which case "...Info" is the best thing I know of. Say FileInfo or ProgramInfo (e.g. a CNC machine program).
- Lio 6y agoWorst variable name I’ve ever seen was “data2”. There was no longer a “data” in that code but presumably there had been at one point.
- qznc 6y agoLikes more like a list of names to avoid for me. Manager, handler, controller, and service add little to no information. I try to find more specific verbs instead. Distributor is a better one sometimes. For example, ConnectionDistributor instead of ConnectionManager if it accepts connections and distributes them to a thread pool.
- zmmmmm 6y ago> think a thousand times before you name something I gave up .... I write mostly in languages that allow you to deterministically rename things with refactoring tools, so I frequently rename important classes 5 or 6 times before I'm done.
- golergka 6y agoThis. Not only renaming, but re-bundling entities, moving layers, changing abstractions – refactoring is crucial during and immediately after development. As you implement your idea, you will inevitably find a better way to express it, and it's crucial to be able to re-do these things as many times as possible to reach the best possible result (if it's not a throwaway prototype, of course): no maintainer, including yourself a month later, will have a picture as full and clear as you right after finishing the first iteration.
- marco_craveiro 6y agoCompletely agree. I find that the right name takes ages to come about. Worse, it may require a lot of architectural refactoring that, many times, has nothing to do with the one entity you are trying to name. Instead, it is connected with the entire workflow you are designing. Nothing worse than spending ages coming up with the right name for a class, only to find out the entire class is not needed and you got the workflow all wrong :-) which I have done many a time, to be fair
- johnnylambada 6y agoI've been around long enough to remember a time before refactor->rename was a thing. Now naming can evolve as the class/variable evolves so there's so much less initial cognitive overhead worrying about a name than there once was.
- fabb64 6y agoPure functions can also easily be memoized which can speed up processing.
- nottorp 6y agoThis reminds me of: https://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo...
- 0x445442 6y agoThe thing is a proper REST API is a Kingdom of Nouns. The core issue is that very few were doing object oriented programming and design the way it was intended. Giving me some leeway here, Kay's original idea was a programming model similar to that of the internet but in memory. If one's position is that striving for a distributed model in memory is the wrong approach because of the inherent complexity associated with distributed systems that's fine but I rarely hear this argument. Typically, OOP/OOD is criticized based off of patterns that aren't proper.
- 0x445442 6y agoIt's an old talk but it zero's in on a point that is tremendously over looked in discussions about OOP/OOD. https://www.infoq.com/presentations/Making-Roles-Explicit-Udi-Dahan/ https://www.infoq.com/presentations/Making-Roles-Explicit-Ud...
- nendroid 6y agoIt's not just pure functions. Two things break modularity: Free variables and mutation. The problem with OOP is that no method is truly pure, no method is a combinator. class Thing var memberVar def addOne(): return memberVar+1; The above is an example of your typical class. AddOne is not modular because it cannot be used outside of the context of Thing. You can use static functions but the static keyword defeats the purpose of a class and makes the class equivalent to a namespace containing functions. class Thing static def add(x): return x + 1; the above is pointless. Just do the below: namespace Thing: def add(x): return x + 1; The point of OOP is for methods to operate on internal state. If you remove this feature from OOP you're left with something that is identical to namespaces and functions. Use namespaces and functions when all you have are pure functions and use classes when you need internal state... there is literally no point for OOP if you aren't using internal state with your classes.
- oaiey 6y agoSo applied Onion Architecture. I agree with this part. However, the functional part of the discussion, while I agree with its benefits, is highly depending on the domain within the onion.
- necovek 6y agoGenerally, to those of us who apply the functional approach everywhere, it comes naturally whatever the problem. There are idiomatic ways to program in particular languages (even in Python or JavaScript) which are strictly against the functional approach, even though nothing in those languages prohibits it. It gets trickier with external dependencies which are "forced" on you too. Do you have a concrete "domain" example that you think will be hard to turn functional (which basically means turn the integration points into minimal functions doing just the integration bits — basically, any side-effects are limited to those integration functions)?
- oaiey 6y agoA shadow DOM. A state manager ;). A protocol implementation which needs state. There are cases where the domain has state. Like said, typically, for request/response cases which is 90% of everything we program nowadays, this state is typically loaded from somewhere else. I am also not particular arguing for OOP here. It is just the absolutism which are an issue.
- t0astbread 6y agoWhat about an "update(state, event) -> NewState" design for state machines and stateful protocols?
- necovek 6y agoA DOM is a huge hierarchical data structure and not much else (sure, it has function pointers too, but that's pretty much it). You can easily turn things into substructures and have functions only work on those: whether you pass by reference or value is up to you and your choice of language, but even when passing by reference (to avoid memory copying), you can write functional code. Again, it sure is non-idiomatic for most languages, but that does not mean it's impossible or even hard. As far as "absolutism", generally, aiming for minimal statefulness will help you write more maintainable code (citation missing :).
- otabdeveloper4 6y agoWay off the mark. "Software architecture" is about how you make the disparate parts of a software system work together towards a common goal. Most of these parts aren't code: hardware, programmers, tech support, HR, licensing and legal, etc.
- scsilver 6y agoCompletely agree, software does what its told, thats easy, driving consensus across people is the hard part
- username90 6y agoIf it was easy why don't you just implement the solution directly instead of spending time in meetings discussing how to implement the solution? The reason we spend time caring about software architecture is because writing software is hard and anything that can make it simpler is very helpful, like structuring its architecture beforehand.
- solipsism 6y agoSoftware architecture involves HR? You can just change around definitions all you want, but don't expect the rest of the world to agree.
- qznc 6y agoThere is no consensus about the definition of software architecture. http://beza1e1.tuxen.de/definitions_software_architecture.html http://beza1e1.tuxen.de/definitions_software_architecture.ht...
- username90 6y agoNone of those includes human resources like programmers as a part of software architecture.
- otabdeveloper4 6y ago
- layoutIfNeeded 6y agoI find it amusing how these software architecture gurus always demonstrate their teachings with a cookie-cutter CRUD app. There are software which do things other than making REST calls... Show me how you’d implement a basic MS Paint clone and we can talk!
- tarruda 6y agoI think the idea described by the op is how you're supposed to organize code when programming in Haskell. Having most of your application logic in pure functions makes it easy to test and reason about. While this is possible to do, it is easier said than done. Organizing the code this way requires a lot of time dedicated to thinking/rewriting, which is often not available in the usual corporate environment with strict deadlines.
- necovek 6y agoI think it only requires a change of mindset for a developer. Learning TDD truly will usually lead to functional code, even if you do not write your tests first. Eg. I can't imagine what would be hard for a MS Paint clone to achieve using this approach. There are always things with side-effects (IO, namely), agreed, but in this CRUD example, the integration bits are coupled in a way where you need a single trivial integration test. Similar approach can be applied to a GUI app, so I am not sure why does GP feel it can't? Any concrete things where you think you can't apply it? Sure, you'll have many more small functions, but I think the core takeaway should be that you can always structure code in away where integration functions are simply statement-lists (eg. no control-flow logic in them). That will usually require rest of your code to be functional. And yeah, it's harder to adapt existing code to this pattern, but introducing new code in an existing code base following functional pattern is trivial.
- tarruda 6y ago> Any concrete things where you think you can't apply it? I'd say that you can apply it mostly everywhere, however it is not friendly to most people that will maintain the code, and that is a problem since most software will have many maintainers over its lifetime. This reply to another comment I made on this thread should elaborate a bit more: https://news.ycombinator.com/item?id=24917764 https://news.ycombinator.com/item?id=24917764
- deleted 6y ago[deleted]
- deleted 6y ago[deleted]
- speedgoose 6y agoI understand that the point of his example is to introduce the reader to a variant of the very classic multitier architecture with layers. https://en.wikipedia.org/wiki/Multitier_architecture https://en.wikipedia.org/wiki/Multitier_architecture But I prefer the first version of the example, because while it's named "complex code" I think it's the simplest version. I think that there is no need to decouple this simple and straightforward function in three functions that cannot be reused independently. Splitting a function in smaller functions make sense where the code is complex, but I think it's important to not overly do it.
- rsa25519 6y agoAgreed. Abstractions are more mentally expensive than a single concrete idea but less expensive than many concrete ideas. When something doesn't need to be abstract, especially in a language where refactoring is easy, it should not be abstract.
- berkes 6y agoOr, how I would put it more pragmatic: the first example is a sure way towards seven slightly different mechanisms to call an Api. Per year. On a team with seven devs.
- mathw 6y agoNot doing the first example is a sure way towards either copying that function and changing it a bit because you need a slightly different call, or a function with a pile of branches in it because it needs to operate in several modes. Both ways could go wrong. I don't care what your architectural choice is, you can still mess it up.
- cies 6y agoI find the Multitier Arch (MA) has much better names. The arch proposed by the article has "Application Business Rules" (ABRs) and "Enterprise Business Rules" (EBRs). Now in a small start-up, what are the "enterprise rules", is "enterprise" not merely a buzzword in this context? And how ABRs and EBRs differ is not well explained. In MA this is much clearer: the Data Access Layer (DAL) contains DAOs (the persistence side of the model in MVC), Business Layer (BL) contains the business/domain logic (the other side of the M in MVC and/or the business logic that may end up in the C of MVC; aka services in Rails), the Application Layer (AL) contains what would be typical "controller logic" (authorization/redirection/data gathering for the presentation) in MVC and the Presentation Layer (PL) contains the V from MVC. > [...] I think it's important to not overly do it. Yups. So maybe one can do without a BL at first, and put the business logic in the DAL at first and dont mind to have a litttttle bit of it creaping into the AL (controller).
- lenkite 6y agoFrankly, find_definition should be additionally modified to take a function that consumes a url and returns json data. This way it's easily unit-testable.
- danuker 6y agoThat would be dependency injection. It is a valid way to design around side-effects. Another (equivalent) way to test it is to mock out requests.get and response.json using a mocking library: instead of performing real requests, do what the test wants (return correct data, return unexpected date, or throw an exception).
- AaronFriel 6y agoA few years ago I was on a team and the dev lead had a practice of sharing Gary Bernhardt's Boundaries talk every time there was some degree of rotation or churn on the team. Almost part of the onboarding. As a functional programming aficionado, I didn't need the sales pitch, but it was the presentation that was gripping. Keep in mind, Ruby isn't a functional language, but here was this presenter describing essentially how you write good Haskell code, but in Ruby! A language that makes it so easy to mutate in place that it's known for libraries that "monkey patch" base classes and can change the definition of operators and functions at runtime! Fantastic talk, fantastic presentation. I now share it with my new engineering colleagues too. Here's the talk where he talks about imperative shell, functional core, "Boundaries": https://www.youtube.com/watch?v=yTkzNHF6rMs https://www.youtube.com/watch?v=yTkzNHF6rMs
- Huggernaut 6y agoI think Gary shared a Twitter related project that was written in this way, but do you have any examples of other projects by any chance?
- dnautics 6y ago"most projects in elixir". Some good open-source examples are oban, hex.pm, papercups.io
- AaronFriel 6y agoAh, I am blocked by Gary on Twitter and there is some irony that his Boundaries talk culminates in a Twitter client. (Gary if you're reading this I enjoyed your feed!) I'm not aware of any good OSS examples, either. I think React and Redux are in a way an implementation of these concepts. React is a view layer that sends messages and each component is (ideally) a pure function of state, and sometimes its own history. It's so, so easy to unit test the interactions. Hooks make it a little harder to reason about, and it's unfortunately easy to write hooks that perform IO and you suddenly end up in a miasma of difficult-to-test code where you have to return to using a mocking library to "replace" IO functions with fake versions, and then you're on a slippery slope again toward mixing interaction and mutation in one layer.
- aryehof 6y agoTo me, a software architecture is an implementation of the decisions made (amongst competing choices) in meeting its requirements, both functional and non-functional. I simply don't find the article to provide a grand unified theory across different genres of software at all. What worries me is that newcomers to the industry will, until they move to the next discovered silver bullet unified theory.
- brabel 6y agoIt's a good theory for writing general code that can be easily tested and modified in the future. This is the first thing you need if you want to meet any business requirement at all.
- Cthulhu_ 6y agoWhile I agree there is no silver bullet, most applications will have a lot of similar nonfunctional requirements - API, data processing, and i/o. Having a common basis to go to - then evolving it - is a good practice. Cowboying or reinventing the wheel is not a good practice.
- makach 6y agoWe have built software for a long time and we have learned a lot. There are many good ways (patterns) to reuse when you need to solve a particular problem. Uncle Bob speaks the truth. For me, the challenge has never been with the architecture or our combined technical knowledge. What I have observed as the main challenge is that most technologists start solving the problem before they know what the problem is.
- Spearchucker 6y agoThis, so very much. It helps to spend time understanding a situation before assuming you have appropriate ideas about changing it. Management consultants (good ones may be rare, but they exist) have some pretty complex process models just to get a handle on the problem.
- antupis 6y agoPersonally I have found that actually understanding situation means that I write code that solves it.
- username90 6y agoIf the people asking you to solve a problem don't know what problem they want solved then there isn't much you can do except try to solve something and see if they complain. You could argue that you should talk more with the stakeholders, but most of them don't have the skills required to accurately identify their own problems. Instead they need to see something running and then notice when things are missing, which is why we start building stuff without knowing what problem we ultimately are supposed to solve.
- makach 6y agoAn business analyst appeared in the wild (..) as a consequence of stakeholders not knowing or having the necessary skills. BAs are experts in illiciting user requirements and transforming them into specifications and goals you can use to solve a problem. Unless you know your goal you will never be able to deliver on your customer's expectations.
- OneGuy123 6y agoAnother "academic level" demonstration that doesn't work in practice. It's so simple to say "look how awesome this approach is" on a 50 line program, "just use pure functions everywhere". In real life things get very complex because there are 200 working parts interconnected. And not because "it's bad design". But because that is the requirement. Soon you get pure functions with tons of parameters or parameters that are complicated classes/structs themselves because the work that needs to be performed is very complicated. Then you get to the issue of "this function gets the entire class as the input param but it only access a small amount of members in that class". "Yo bro just split the class into multiple smaller ones". Sorry bro can't do, those separate classes will need to be acccessed as a whole sooner or later in another part. People act as if there will ever be a thing such as a "perfect programming design". There won't because things will always evolve & change. Real life programs are simply too complex.
- danuker 6y agoSure, real life programs are very complex. But that doesn't stop you from testing and refactoring whatever local point you need to touch, so that you can get the job done easier. My hunch is that moving pure-functions and pure-objects out of the spaghetti will gradually eat away at the spaghetti.
- andrewjl 6y ago> In real life things get very complex because there are 200 working parts interconnected. And not because "it's bad design". But because that is the requirement. These sorts of things are always better discussed in terms of specific cases instead of generalities, but I think one of the key parts of good software engineering is factoring the requirements into the simplest design possible. And even when time or other considerations prohibit creating an extensive design ahead of time, practicing a few well-chosen heuristics like "use composition", "keep functions and methods short", "use clearly defined and consistent terminology in your abstractions" can go a long way to making code less unruly and easier and cheaper to refactor down the road.
- kriro 6y agoIs it just me, or does it feel a bit "dishonest" to have """data = requests.get(url).json()""" in example 3 but use two lines for that in examples 1.
- goto11 6y agoThe fundamental problem of writing about architecture: The concerns about decoupling and layering is only relevant in complex software. But an article need to present simple examples to be readable. In this case, the first listing is simple and easily readable.
- nxpnsv 6y agoNice and all, but It feels quite pompous to tout this as a grand unified theory of anything. This is a highly specific little corner of software architecture, and is neither grand, unified or even a theory.
- bilekas 6y agoI support this a bit, I would have different idols in mind, but I wont fault someone for theirs. Once something is learnt it really doesn't matter. The pain is the title..
- nxpnsv 6y agoYes, sure a little unfair and nitpicky, but still the title baited me to click and left me unimpressed. I wonder if my impression had been better if the content matched the expectations.
- froman88 6y agoFrankly, if you really thought a single article could deliver a "grand unified theory" of anything, that's on you. People pick catchy titles; it's a key part of getting noticed these days.
- nxpnsv 6y agoI have a past as a physicist, so I am a little sensitive to that phrasing. Optimally a catching title matches relevant content - probably get lower bounce rate that way too...
- swaits 6y agoAnd we have a word for it, clickbait. It’s okay that it exists. It’s also ok for us to call it out when we see it.
- ChrisMarshallNY 6y agoI tend to write in a modular, layered fashion (I call it the “layer cake” pattern), and I like to try breaking up large methods to reduce CC. For example, I might break a large switch statement up into “sets” of handlers. The main motivation for this, is because I use what I call “evolutionary design.” I tend to refine design as I progress through development, as opposed to having it substantially complete at the start (A lot of classic developers probably just defecated masonry at the very thought of that, but it WFM). Having a finer granularity goes a long way, in supporting this methodology. It also helps a lot for refactoring, improvements, and testing. The overall quality of my products is drastically enhanced by modularity.
- std_badalloc 6y agoI don't think the example here is the best. There's a case to be made for extracting pure functions and organizing them like this, but I don't think this code makes it. The benefit of pure functions IMO is primarily in that the code becomes easy to reason about if it doesn't depend on state. But any app that does anything will have state, and the question is how you manage that. One guideline could be that individual code units should reduce the amount of state you need to worry about at higher levels of abstraction. In the example, there is hardly any code that does anything different depending on state. There's no state being managed, so there isn't actually any architectural problem being solved here. Should the API go down or change its format, the code breaks. The pure pluck_definition() will still fail to parse the JSON if the format changes. The pure build_url() will stop working if the API changes its URL format. They will pass unit tests, but fail in practice. An actual problem to be solved here is to abstract away the details of the REST API, formatting and network errors. One way to do this is to pack that into a component with a well defined interface. You can still do this stateful/non-stateful split within the component if you want, but on the application level you need to apply that heuristic recursively at different levels of abstraction.
- whoomp12342 6y agoThere is absolutely a problem here. Having worked in disasters of a code base, the architectural pattern in the first example is probably fine... until the software grows. The first function truly is a thing-do-er which violates SRP. Then it will easily become a ball of mud. Why is this so bad? Its not because its expensive, yes that is bad, but the largest issue with working in a ball of mud architecture, is that the code becomes so fragile and interdependent that changing any one thing can easily lead to breaking many other things. This leads to a culture of fear of change which grows tech debt. Then one day someone steps up and decides to actually refactor this ball of mud to have some semblance of logic to it, what a noble soul. That person is then subject to a barrage of bugs and issues from the refactor and is that the mercy of their supervisor. Dealing with state and other side effect like issues is certainly something to consider in architecture, but it is a different argument entirely.
- irjustin 6y agoAlong the same lines as the article, I've started thinking about internal code as ETL - all code. There is some faucet, transformation and then a sink. Only at the sink does external state get mutated. It helps because transformations can be closer to pure functions and you know there is no state changes until you've hit the sink/loader. Not sure yet, I haven't concluded it's the right way for me, as it has saved a few headaches here and there - but I'm sure I've unknowingly caused more else where just yet unseen. Also for the author, my favorite word of idempotent - given the same inputs - always get the same output.
- justincredible 6y agoIdempotent means feeding the output back in as the input will result in the same output. It might require purity, but is conceptually distinct. For example, boolean negation is a pure function as it depends only its input and has no side-effects, but it's not idempotent since: not(not(var)) != not(var)
- augustk 6y agoWhen I see a subroutine with a verb in its name I think of side-effects. In my book a pure function should be named after the result it returns, so in this case I would use the name `definition' instead of `find_definition' and `definition_url' instead of `build_url'. For predicates I try to avoid an "is" prefix when a simple adjective is sufficient.
- stingraycharles 6y agoI don’t think this is true at all. The most functional, side-effect-free functions are often just verbs (map, reduce, concat, etc).
- barrkel 6y agoMap, reduce, filter, fold, project, transform, group_by, bind, apply - any functional API you care to look is all verbs. Functions do work. That doesn't mean they have to have side-effects; if they're functional, they do work on the input and produce output. Doing is a verb. It's natural. Using a noun as a function name is at best justified when you have a situation where you want to hide whether data is being calculated on demand, or fetched from some storage or lookup table. Some languages bake this in, in the form of properties - attributes of a structure which look like fields, but are actually functions. Those things have nouns as names.
- augustk 6y agoIt depends on the culture and the programming language. The advantage of using noun phrases for pure functions is that 1. only by reading the name of the function you know it's a pure function 2. a call to a pure function in an expression reads more naturally since operands are values and values are nouns.
- ivanche 6y agoToo bad you're heavily downvoted. Principle "function name is a noun if it returns something, and verb if it doesn't" is super useful - just by looking at its name you know immediately if it has side-effects. Since I learned it I apply it all the time in programs I write alone, but almost always I see pushback in a team because people are unfortunately used to see `getX`, `fetchY` etc. as method names.
- quantum_state 6y agoIt seems to be a joke to claim such as “grand unified theory of ...”
- elonmusk53 6y agoThis is great— thanks for sharing
- iainmerrick 6y agoThis is good advice and a good writeup, but I take exception to one (boldface!) line: Coupling kills software I hear this a lot but I think it’s highly overstated. The “clear” final version is still strongly coupled -- you can’t call find_definition without it directly calling the build_url helper! The key aspect isn’t the coupling, it’s that the high-level imperative function is calling a simple, low-level, testable helper. Coupling can be bad, sure, but not if it’s kept under control. And excessive decoupling can make programs much harder to understand and debug! Have you ever worked on an app where every reference to another object is injected via a DI framework, and where all the significant calls that actually do stuff are farmed out to asynchronous messages and callbacks, all in the name of decoupling and testability? That can make it really hard to debug problems. A good balance is what’s needed, not decoupling over all other concerns.
- tonyarkles 6y ago> every reference to another object is injected via a DI framework And it's injected with an interface, not a concrete class, so you have to go on a giant easter egg hunt to even figure out which class is being injected... only to eventually discover that there's only one class that implements that interface.
- PaulKeeble 6y agoBut then despite that the class is getting a generated proxy that does basically nothing or is causing the problem and it is really hard to determine anything except tracing it at runtime to find out what actually is being run. People do some crazy things with Spring and interface injections in Java and it can get really Opaque quickly but also often it really is just 1 class implementing an interface and there are no tests using the interface at all.
- tonyarkles 6y agoFor the one project I worked on that was Spring-based, my joke was that Spring was a really effective way to convert compile-time errors into run-time errors :)
- w_t_payne 6y agoWith this architecture, it feels like the very outermost level - the framework and drivers - would ideally have little or no application-specific logic in it whatsoever, and would exist simply to glue together the various functional components with I/O and distribute them to whatever computational resources are required for their execution. This, to me, feels very very similar to a model-based approach, where the outermost level is a modeling framework that does nothing more than route data between different functional components and IO components. I have a strong hunch that this outermost layer does not need to consist of anything more than a suitable framework plus some configuration data specifying (a) the mapping from functional components to hardware resources and (b) the data flow between functional components. If this is indeed the case, then, extracting individual components or subsets of the system for testing should simply be a matter of providing a suitable transformation of the configuration data, and re-executing the framework.
- adamkl 6y agoI spend a lot of time thinking about these sorts of topics (actually, I just taught a 4 hour session yesterday that used most of the terms in this article), working with newer, less experienced developers, and trying to figure how to distill the essence of "architecture" down to something simple that everyone can start with. This is what I’ve started telling people: Use mostly functions, try to make most of them pure. I think that can get people (even new devs) 80% of the benefits (testability, composability, loose coupling, and the ability to reason about code) of more complicated, prescriptive architectures (Hexagonal, Onion, Ports & Adapters, Clean, etc) with a minimal amount of ramp up. Of course, this isn't the solution to every problem (it obviously depends on the domain you are working in, for me its webDev and backends), but I think maybe its a good way for people to start. Edit: Here is a great talk demonstrating that by following a simple functional approach, your code can naturally fall into a “pit of success”: https://youtu.be/US8QG9I1XW0 https://youtu.be/US8QG9I1XW0
- jhardy54 6y ago> Use mostly functions, try to make most of them pure. This reminds me of: > Eat food, not too much, mostly plants > > -- Michael Pollan My new mantra: Write software, not too much, mostly functions.
- AnimalMuppet 6y ago"Not too much" is interesting. If I understand you correctly, you can write "too much software". I can think of at least three ways - bad architecture, too little abstraction forcing repetition, and just bad writing. Did you have something else in mind here?
- adamkl 6y agoI believe “Not too much” is a simple reminder that more code equals more bugs. So, try to write less code whenever possible.
- asimpletune 6y agoI think more likely is to actually have too much abstraction.
- gwbas1c 6y ago[Meta] The introduction to the article relies on so much context and assumed knowledge that I almost didn't read past the third paragraph. Who are Uncle Bob, Gary Bernhardt, and Mr. Brandon Rhodes? I have no idea, and I don't need to know who they are to read the rest of the article. (Which itself is extremely well-written.) IMO: Have an introduction that doesn't rely on unneeded context. It's appropriate to credit people; but do it in a way where a reader unfamiliar with the context doesn't assume that they need to know the context to read further.
- mac01021 6y agoThe author could stand to flip the sentences around a bit, but the introduction is basically trying to say "these three thoughtful guys collectively formulated some useful ideas about software architecture and in this post I will present those ideas". It is important too attribute ideas to their originators (or at least the source from which one has learned them) and it is good that the OP is doing that. I agree it could have been done a little more gracefully, though, with just a minor tweak to use phrasing that doesn't seem to assume the reader is familiar with those dudes.
- deleted 6y ago[deleted]
- humbleMouse 6y agoMy theory is software should be architected with debugability, deployability, and developer friendliness in mind. If you can't deploy your solution on a predictable schedule, with multiple teams working on it in parallel, and keep stakeholders happy, then you are on a fast path to irrelevance no matter how "sophisticated" the architecture is.
- billfruit 6y agoBut its all rules of thumb and common sense, largely. When are we going to get an axiomatic theory of software engineering/architecture which shall allow us to argue about and compare different architectures for a solution and arrive at an ideal one?
- UK-Al05 6y agoI don't think you'll be able to do that. What you can is make is define a starting point app in the desired architecture. Then create a list of functional changes to go from starting point, to desired state. Then you can use various stats for each architecture on how complex was the changes. Make the functional starting point, the functional list of changes and stats a standard.
- zeroonetwothree 6y agoI prefer the first example. It’s more straightforward and avoids accidentally creating dependencies (for example some other code calling pluck_definition) that will be make it harder to modify when you need to add features or the API changes. Testing pluck_definition by itself is completely pointless since it does nothing on its own. This is the “test public interfaces, not private implementation” principle. Similarly build_url and pluck_definition need to be coupled because they both depend on the specifics of a third party API. It makes it much more clear what the expected output is to keep them together so that if something breaks you know which url to check and what the response should look like. I also dispute that it’s hard to unit test the first function—-you would simply mock the api response and then you have a great test. Much better than having separate tests for tiny helper functions that do nothing on their own. Now maybe this example is just too simple and the presented architecture makes more sense on larger code but if so then the article is poorly written. Examples need to be realistic enough not to obfuscate.
- acdha 6y ago> I also dispute that it’s hard to unit test the first function—-you would simply mock the api response and then you have a great test. How many times have you seen a test pass or fail when it shouldn’t have because the mocks didn’t match the actual code? It’s a useful technique but it has drawbacks which are not easily prevented.
- yeswecatan 6y agoYou would still need to use mocking to test the imperative shell
- danuker 6y agoThis, here, is what I meant!
- jakeva 6y agoWe use mocks a lot on my team, and I don't think we've ever seen what you describe. Maybe you're using mocks incorrectly?
- brundolf 6y ago"Write code. Not too much. Mostly functions."
- BenoitEssiambre 6y ago"Functional Core / Imperative Shell" is also why I love a combination postgresql/nodejs stack. The transactional sql or plpgsql surrounding the state makes dangerous operations much safer and clean. Then as you get farther from risky state changes, you get the productive flexibility of less strict javascript. You also get the option of putting complex operations in plpgsql functions when you want them executed atomically without any side effects, corruption, or cleanup requirements when one of the step fails. Your default failure mode becomes "You got an error, everything was automatically reverted, you can safely tweak your request and try again". Also operations being performed in a monolitic db means that the different pieces of data are much more likely to be loaded in adjacent memory caches and ready to be joined, combined and processed, providing significant efficiency and speed gains.
- kyberias 6y agoRunning code in a database is basically the opposite of the software architecture covered in the article.
- SPBS 6y agoThe only mantra about code organization I subscribe to is that it should be as simple as possible. Sometimes it means using several layers of abstraction like in this article. Usually it means just writing the damn thing in the most straightforward way because simple implementations are easy to adapt for future changes.
- AnimalMuppet 6y ago"Most things aren't rocket science."
- ManuelKiessling 6y agoShameless plug of my 2012 blog post where I demonstrate The Clean Architecure for Go applications: https://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/ https://manuel.kiessling.net/2012/09/28/applying-the-clean-a...
- danuker 6y agoThank you! You have gone much deeper.
- jfarmer 6y agoI always really liked Avdi Grimm's "Confident Code" talk: https://www.youtube.com/watch?v=T8J0j2xJFgQ https://www.youtube.com/watch?v=T8J0j2xJFgQ It has similar conclusions, but does it from the frame of the "story of the code" rather than abstractions like "functional core" vs "imperative shell"
- maletor 6y agoI had discovered this recently too and asked a relevant StackOverflow question about how people deal with lots of I/O. https://softwareengineering.stackexchange.com/questions/416073/functional-architecture-with-lots-of-i-o/416105?noredirect=1#comment919330_416105 https://softwareengineering.stackexchange.com/questions/4160... The premise is the imperative shell can become pretty riddled with decisions if your application contains a lot of I/O. Some like the "Free Monad" solution, but I found that too be too coupling.
- christiansakai 6y agoI think what I would need mostly is: - dependency inversion, try to push decision as later as possible by using this technique + interfaces - separate pure functions vs side effects functions - static typing that can help me with structs and interfaces with optional value It ends up with a lot of typing but I think it is worth the trouble. Down the road it is easier to maintain and it already saves you time anyway.
- theptip 6y agoThe author makes an observation that has been growing on me in the last few years; many ORM models really prevent you from using an architecture like this, because your “pure core” actually has to have logic for DB operations like saving/filtering. It’s possible to build a DDD Repository in Django or Rails, but really a lot of work. I think the frameworks like sqlalchemy and NHibernate seem to do things a bit better by tracking “dirty” models, hiding the “ORM-ness”, and letting the higher levels control when to flush/write to the DB. But the more you abstract this stuff, the more you lose the auto generated sugar that makes frameworks like Django so productive.
- danuker 6y agoAbsolutely. [Uncle Bob is clear](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-a...) that the inside circles must not know about the outside world. That is, your business logic is not allowed to know what a DB operation is, nor should it do any DB operations behind the scenes. > Frameworks and Drivers. > The outermost layer is generally composed of frameworks and tools such as the Database, the Web Framework, etc. Generally you don’t write much code in this layer other than glue code that communicates to the next circle inwards. > This layer is where all the details go. The Web is a detail. The database is a detail. We keep these things on the outside where they can do little harm.
- theptip 6y agoThe tension I observe is: when should you use a highly productive and well-known framework like Django or Rails, and when should you build your own DDD/Clean architecture? This discussion has gone on for a long time, e.g. see the back-and-forth around https://dhh.dk/2014/test-induced-design-damage.html https://dhh.dk/2014/test-induced-design-damage.html after Weirich demonstrated what sort of thing is required to use the Hexagonal Architecture with Rails. I believe that if your system is sufficiently complex, you'll start to see the benefits of a more structured architecture, but it's a net drag on productivity for small projects. These days perhaps you chop your monolith into microservices before reaching the ROI point for Clean/Hexagonal? I could believe that a "framework-first" architecture is more productive while you have <100kloc. Or maybe it's <10kloc, I don't know. I'm certainly seeing the architectural strain with a Django-first architecture in the current 150kloc monolith I'm working on, and chopping into a few services makes sense there for other reasons, so it might obviate the need for a bigger refactor onto Clean/Hexagonal.
- gen220 6y agoSometimes, the complexity of the world you're modelling requires there to be many, many `build_url`s and `pluck_definition`s piping into and out of each other. I've found these organizing principles to be quite useful. 1. identify the key that your application is processing (in this case, it's `word`) 2. make it so that as many functions as possible take this "key" as their only argument. 3. whenever you are fetching data about a model, it is either directly or transitively in terms of this key. 4. push model-fetching (technically the I part of I/O) as far to the leaves of your program-tree as possible, hiding them behind "model client" classes, that are passed in to your job at construction. 5. Perform all upserts idempotently, atomically, and in the bottom-right corner of the execution tree. This makes it easy to reason about mutations, and also easy to omit when you're wanting to do "dry runs" or read-only local runs. 6. (4) means you will occasionally fetch the exact the same model more than once in the scope of one execution. This is OK. You can optimize this later with execution-tree-scoped caching. With this approach, the user-directed I/O (imperative shell) is at the root of your tree, and very narrowly-defined to be the key of computation. You can build a run-loop on top of this, or a CLI tool, or an rpc service. It's narrowness is kind to all kinds of interfaces. The functional core is everything that isn't the leaves of the execution tree. These functions essentially compose model client calls, and only take the keys as inputs. The leaves are a collection of reusable one-to-five-liner RPC requests / data base queries. If you like, you can wrap these in a class that caches the queries, as described in (5). The tests for leaves mock nothing if they target a DB. They mock the rpc service if they target an RPC service. The tests for the functional core functions mock the model-clients (leave functions). With this structure, as long as your tests are brittle enough to break when an RPC contract or data model changes, you do not need integration tests. I do this in Python, but it would work just as well in any language with structs/interfaces. I've observed the same benefits proclaimed in the article, although my approach is a bit different. If people are interested, I could write more on this, with examples. Let me know!
- nendroid 6y agoStop calling this theory. This isn't theory. This is just rules of thumb and an opinion. You want actual theory? Here's theory: http://www4.di.uminho.pt/~jno/ps/pdbc.pdf http://www4.di.uminho.pt/~jno/ps/pdbc.pdf
- dragonwriter 6y agoIt's theory in the analytical/interpretive sense, not the empirical scientific sense.
- nendroid 6y agoThe paper I presented is not theory in the scientific sense. It's theory in the mathematical/logic proof based sense. There are really two uses of the word "theory" for this context, science based theories proven by statistical experiments, and logical based theories created from a set of elements and assumed axioms. Things like this shouldn't be titled with "Theory." Call it your "opinion" or a "design pattern." Theory is associated with things like the theory of gravity (science) or game theory (math). This title is wildly inappropriate and shows a lack of understanding of what constitutes a theory. The fact that this post is voted up shows how much the public misunderstands "theory" and how they mistake things like this which is actually just someone's qualitative opinion with the integrity of an actual theory. Using big words and drawing diagrams does not lend any formal legitimacy to your opinion.
- jeff-davis 6y agoSoftware architecture, as I understand it, applies to a particular software product/solution. Architecture is your opportunity to choose which problems will be easy and which problems will be hard. For example, a microservices architecture makes HA easy but global consistency and data locality/caching hard. A centralized database architecture is the mirror image. The article discusses architecture in the abstract, which sounds more like a programming model or paradigm (OOP, functional, actor model, imperative), or maybe a pattern. Wouldn't it be weird to discuss building architecture in the abstract, without a notion of the site to build on or the purpose of the building?
- Supermancho 6y ago> data = response.json() That isn't IO, that's encoding. Don't you want to verify you are receiving json (whatever interface you are assuming is available) in pluck_definition? How far down do you want to go with this?
- FowlSoft2013 6y agoI think software architecture is the wrong word for this. It’s module structure, architecture to me includes all the surrounding bits concerning the “ilities”. (Availability, Interoperability, Modifiability, Usability, Testability, Security, Performance). I say this because getting the module structure right is important but not the only factor to a successful application. My favorites in regard to the topic of module structure are David Parnas and Juval Lowy